mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-21 06:19:12 +00:00
Merge pull request #378 from alexbakker/better-errors
Improve overall exception handling and error feedback to the user
This commit is contained in:
commit
8ff817856e
21 changed files with 544 additions and 465 deletions
|
@ -16,7 +16,11 @@ import androidx.annotation.RequiresApi;
|
|||
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
|
||||
|
||||
|
@ -25,6 +29,7 @@ import java.util.Collections;
|
|||
import java.util.List;
|
||||
|
||||
public class AegisApplication extends Application {
|
||||
private VaultFile _vaultFile;
|
||||
private VaultManager _manager;
|
||||
private Preferences _prefs;
|
||||
private List<LockListener> _lockListeners;
|
||||
|
@ -35,7 +40,6 @@ public class AegisApplication extends Application {
|
|||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
_manager = new VaultManager(this);
|
||||
_prefs = new Preferences(this);
|
||||
_lockListeners = new ArrayList<>();
|
||||
|
||||
|
@ -58,6 +62,47 @@ public class AegisApplication extends Application {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean isVaultLocked() {
|
||||
return _manager == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the vault file from disk at the default location, stores an internal
|
||||
* reference to it for future use and returns it. This must only be called before
|
||||
* initVaultManager() or after lock().
|
||||
*/
|
||||
public VaultFile loadVaultFile() throws VaultManagerException {
|
||||
if (!isVaultLocked()) {
|
||||
throw new AssertionError("loadVaultFile() may only be called before initVaultManager() or after lock()");
|
||||
}
|
||||
|
||||
if (_vaultFile == null) {
|
||||
_vaultFile = VaultManager.readFile(this);
|
||||
}
|
||||
|
||||
return _vaultFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the vault manager by decrypting the given vaultFile with the given
|
||||
* creds. This removes the internal reference to the raw vault file.
|
||||
*/
|
||||
public VaultManager initVaultManager(VaultFile vaultFile, VaultFileCredentials creds) throws VaultManagerException {
|
||||
_vaultFile = null;
|
||||
_manager = VaultManager.init(this, vaultFile, creds);
|
||||
return _manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the vault manager with the given vault and creds. This removes the
|
||||
* internal reference to the raw vault file.
|
||||
*/
|
||||
public VaultManager initVaultManager(Vault vault, VaultFileCredentials creds) {
|
||||
_vaultFile = null;
|
||||
_manager = new VaultManager(this, vault, creds);
|
||||
return _manager;
|
||||
}
|
||||
|
||||
public VaultManager getVaultManager() {
|
||||
return _manager;
|
||||
}
|
||||
|
@ -67,7 +112,7 @@ public class AegisApplication extends Application {
|
|||
}
|
||||
|
||||
public boolean isAutoLockEnabled() {
|
||||
return _prefs.isAutoLockEnabled() && _manager.isLoaded() && _manager.isEncryptionEnabled() && !_manager.isLocked();
|
||||
return _prefs.isAutoLockEnabled() && _manager.isEncryptionEnabled() && !isVaultLocked();
|
||||
}
|
||||
|
||||
public void registerLockListener(LockListener listener) {
|
||||
|
@ -79,7 +124,7 @@ public class AegisApplication extends Application {
|
|||
}
|
||||
|
||||
public void lock() {
|
||||
_manager.lock();
|
||||
_manager = null;
|
||||
for (LockListener listener : _lockListeners) {
|
||||
listener.onLocked();
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@ package com.beemdevelopment.aegis.crypto;
|
|||
|
||||
import android.os.Build;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException;
|
||||
import android.security.keystore.KeyProperties;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
|
@ -12,6 +13,7 @@ import java.security.KeyStore;
|
|||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.ProviderException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Collections;
|
||||
|
@ -21,8 +23,6 @@ import javax.crypto.KeyGenerator;
|
|||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class KeyStoreHandle {
|
||||
private final KeyStore _keyStore;
|
||||
private static final String STORE_NAME = "AndroidKeyStore";
|
||||
|
@ -61,6 +61,14 @@ public class KeyStoreHandle {
|
|||
.build());
|
||||
|
||||
return generator.generateKey();
|
||||
} catch (ProviderException e) {
|
||||
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
|
||||
// so if this was caused by a KeyStoreException, throw a KeyStoreHandleException instead
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof KeyStoreException) {
|
||||
throw new KeyStoreHandleException(cause);
|
||||
}
|
||||
throw e;
|
||||
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
|
||||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
|
@ -88,18 +96,14 @@ public class KeyStoreHandle {
|
|||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
|
||||
// try to initialize a dummy cipher
|
||||
// and see if KeyPermanentlyInvalidatedException is thrown
|
||||
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
|
||||
// apparently KitKat doesn't like KeyPermanentlyInvalidatedException, even when guarded with a version check
|
||||
// it will throw a java.lang.VerifyError when its listed in a 'catch' statement
|
||||
// so instead, check for it here
|
||||
if (e instanceof KeyPermanentlyInvalidatedException) {
|
||||
} catch (InvalidKeyException e) {
|
||||
// some devices throw a plain InvalidKeyException, not KeyPermanentlyInvalidatedException
|
||||
return true;
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ public class MasterKey implements Serializable {
|
|||
|
||||
public MasterKey(SecretKey key) {
|
||||
if (key == null) {
|
||||
throw new NullPointerException();
|
||||
throw new IllegalArgumentException("Key cannot be null");
|
||||
}
|
||||
_key = key;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ public class GoogleAuthInfo {
|
|||
builder.authority("hotp");
|
||||
builder.appendQueryParameter("counter", Long.toString(((HotpInfo)_info).getCounter()));
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", _info.getClass()));
|
||||
}
|
||||
|
||||
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
|
||||
|
@ -55,7 +55,7 @@ public class GoogleAuthInfo {
|
|||
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
|
||||
Uri uri = Uri.parse(s);
|
||||
if (uri == null) {
|
||||
throw new GoogleAuthInfoException("bad uri format");
|
||||
throw new GoogleAuthInfoException(String.format("Bad URI format: %s", s));
|
||||
}
|
||||
return GoogleAuthInfo.parseUri(uri);
|
||||
}
|
||||
|
@ -63,13 +63,13 @@ public class GoogleAuthInfo {
|
|||
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals("otpauth")) {
|
||||
throw new GoogleAuthInfoException("unsupported protocol");
|
||||
throw new GoogleAuthInfoException("Unsupported protocol");
|
||||
}
|
||||
|
||||
// 'secret' is a required parameter
|
||||
String encodedSecret = uri.getQueryParameter("secret");
|
||||
if (encodedSecret == null) {
|
||||
throw new GoogleAuthInfoException("'secret' is not set");
|
||||
throw new GoogleAuthInfoException("Parameter 'secret' is not present");
|
||||
}
|
||||
|
||||
// decode secret
|
||||
|
@ -77,13 +77,17 @@ public class GoogleAuthInfo {
|
|||
try {
|
||||
secret = Base32.decode(encodedSecret);
|
||||
} catch (EncodingException e) {
|
||||
throw new GoogleAuthInfoException("bad secret", e);
|
||||
throw new GoogleAuthInfoException("Bad secret", e);
|
||||
}
|
||||
|
||||
// check the otp type
|
||||
OtpInfo info;
|
||||
try {
|
||||
String type = uri.getHost();
|
||||
if (type == null) {
|
||||
throw new GoogleAuthInfoException(String.format("Host not present in URI: %s", uri.toString()));
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "totp":
|
||||
TotpInfo totpInfo = new TotpInfo(secret);
|
||||
|
@ -105,13 +109,13 @@ public class GoogleAuthInfo {
|
|||
HotpInfo hotpInfo = new HotpInfo(secret);
|
||||
String counter = uri.getQueryParameter("counter");
|
||||
if (counter == null) {
|
||||
throw new GoogleAuthInfoException("'counter' was not set");
|
||||
throw new GoogleAuthInfoException("Parameter 'counter' is not present");
|
||||
}
|
||||
hotpInfo.setCounter(Long.parseLong(counter));
|
||||
info = hotpInfo;
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(String.format("unsupported otp type: %s", type));
|
||||
throw new GoogleAuthInfoException(String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
} catch (OtpInfoException | NumberFormatException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
|
|
|
@ -34,7 +34,7 @@ public class HotpInfo extends OtpInfo {
|
|||
OTP otp = HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
|
||||
return otp.toString();
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -149,7 +149,7 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
|||
* the vault was locked by an external trigger while the Activity was still open.
|
||||
*/
|
||||
private boolean isOrphan() {
|
||||
return !(this instanceof MainActivity) && _app.getVaultManager().isLocked();
|
||||
return !(this instanceof MainActivity) && _app.isVaultLocked();
|
||||
}
|
||||
|
||||
private void setGlobalAnimationDurationScale() {
|
||||
|
|
|
@ -20,34 +20,45 @@ import androidx.annotation.NonNull;
|
|||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.CancelAction;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
|
||||
import com.beemdevelopment.aegis.ui.tasks.SlotListTask;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class AuthActivity extends AegisActivity implements SlotListTask.Callback {
|
||||
public class AuthActivity extends AegisActivity {
|
||||
private EditText _textPassword;
|
||||
|
||||
private CancelAction _cancelAction;
|
||||
private SlotList _slots;
|
||||
private BiometricPrompt.CryptoObject _bioCryptoObj;
|
||||
|
||||
private SecretKey _bioKey;
|
||||
private BiometricSlot _bioSlot;
|
||||
private BiometricPrompt _bioPrompt;
|
||||
|
||||
private Preferences _prefs;
|
||||
private boolean _stateless;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -67,8 +78,21 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
|||
});
|
||||
|
||||
Intent intent = getIntent();
|
||||
_slots = (SlotList) intent.getSerializableExtra("slots");
|
||||
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
|
||||
_slots = (SlotList) intent.getSerializableExtra("slots");
|
||||
_stateless = _slots != null;
|
||||
if (!_stateless) {
|
||||
VaultFile vaultFile;
|
||||
try {
|
||||
vaultFile = getApp().loadVaultFile();
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> onBackPressed());
|
||||
return;
|
||||
}
|
||||
|
||||
_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
|
||||
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
|
||||
|
@ -87,16 +111,16 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
|||
continue;
|
||||
}
|
||||
|
||||
Cipher cipher = slot.createDecryptCipher(key);
|
||||
_bioCryptoObj = new BiometricPrompt.CryptoObject(cipher);
|
||||
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
|
||||
_bioSlot = slot;
|
||||
_bioKey = key;
|
||||
biometricsButton.setVisibility(View.VISIBLE);
|
||||
invalidated = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (KeyStoreHandleException | SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (KeyStoreHandleException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
|
||||
}
|
||||
|
||||
// display a help message if a matching invalidated keystore entry was found
|
||||
|
@ -105,15 +129,14 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
|||
}
|
||||
}
|
||||
|
||||
decryptButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
decryptButton.setOnClickListener(v -> {
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
|
||||
|
||||
char[] password = EditTextHelper.getEditTextChars(_textPassword);
|
||||
trySlots(PasswordSlot.class, password);
|
||||
}
|
||||
List<PasswordSlot> slots = _slots.findAll(PasswordSlot.class);
|
||||
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
|
||||
new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener()).execute(params);
|
||||
});
|
||||
|
||||
biometricsButton.setOnClickListener(v -> {
|
||||
|
@ -121,20 +144,6 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
|||
});
|
||||
}
|
||||
|
||||
private void showError() {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.unlock_vault_error))
|
||||
.setMessage(getString(R.string.unlock_vault_error_description))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
|
||||
.create());
|
||||
}
|
||||
|
||||
private <T extends Slot> void trySlots(Class<T> type, Object obj) {
|
||||
SlotListTask.Params params = new SlotListTask.Params(_slots, obj);
|
||||
new SlotListTask<>(type, this, this).execute(params);
|
||||
}
|
||||
|
||||
private void selectPassword() {
|
||||
_textPassword.selectAll();
|
||||
|
||||
|
@ -147,10 +156,7 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
|||
switch (_cancelAction) {
|
||||
case KILL:
|
||||
finishAffinity();
|
||||
|
||||
case CLOSE:
|
||||
Intent intent = new Intent();
|
||||
setResult(RESULT_CANCELED, intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
@ -159,7 +165,7 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
|||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (_bioPrompt != null) {
|
||||
if (_bioKey != null) {
|
||||
if (_prefs.isPasswordReminderNeeded()) {
|
||||
focusPasswordField();
|
||||
} else {
|
||||
|
@ -195,12 +201,24 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
|||
}
|
||||
|
||||
public void showBiometricPrompt() {
|
||||
Cipher cipher;
|
||||
try {
|
||||
cipher = _bioSlot.createDecryptCipher(_bioKey);
|
||||
} catch (SlotException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
BiometricPrompt.CryptoObject cryptoObj = new BiometricPrompt.CryptoObject(cipher);
|
||||
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
|
||||
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.authentication))
|
||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||
.setConfirmationRequired(false)
|
||||
.build();
|
||||
_bioPrompt.authenticate(info, _bioCryptoObj);
|
||||
_bioPrompt.authenticate(info, cryptoObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -212,8 +230,37 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
|||
}
|
||||
}
|
||||
|
||||
private void finish(MasterKey key, boolean isSlotRepaired) {
|
||||
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
|
||||
|
||||
if (_stateless) {
|
||||
// send the master key back to the calling activity
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("creds", creds);
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
try {
|
||||
AegisApplication app = getApp();
|
||||
VaultManager vault = app.initVaultManager(app.loadVaultFile(), creds);
|
||||
if (isSlotRepaired) {
|
||||
vault.setCredentials(creds);
|
||||
saveVault();
|
||||
}
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.decryption_corrupt_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private class PasswordDerivationListener implements PasswordSlotDecryptTask.Callback {
|
||||
@Override
|
||||
public void onTaskFinished(SlotListTask.Result result) {
|
||||
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
|
||||
if (result != null) {
|
||||
// replace the old slot with the repaired one
|
||||
if (result.isSlotRepaired()) {
|
||||
|
@ -224,14 +271,15 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
|||
_prefs.resetPasswordReminderTimestamp();
|
||||
}
|
||||
|
||||
// send the master key back to the main activity
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("creds", new VaultFileCredentials(result.getKey(), _slots));
|
||||
intent.putExtra("repairedSlot", result.isSlotRepaired());
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
finish(result.getKey(), result.isSlotRepaired());
|
||||
} else {
|
||||
showError();
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
|
||||
.setTitle(getString(R.string.unlock_vault_error))
|
||||
.setMessage(getString(R.string.unlock_vault_error_description))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
|
||||
.create());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -247,7 +295,19 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
|||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
super.onAuthenticationSucceeded(result);
|
||||
trySlots(BiometricSlot.class, _bioCryptoObj.getCipher());
|
||||
|
||||
MasterKey key;
|
||||
BiometricSlot slot = _slots.find(BiometricSlot.class);
|
||||
|
||||
try {
|
||||
key = slot.getKey(result.getCryptoObject().getCipher());
|
||||
} catch (SlotException | SlotIntegrityException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(AuthActivity.this, R.string.biometric_decrypt_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
finish(key, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -2,6 +2,8 @@ package com.beemdevelopment.aegis.ui;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.text.Editable;
|
||||
|
@ -16,17 +18,18 @@ import android.widget.CheckBox;
|
|||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
|
@ -123,7 +126,7 @@ public class Dialogs {
|
|||
|
||||
char[] password = EditTextHelper.getEditTextChars(textPassword);
|
||||
PasswordSlot slot = new PasswordSlot();
|
||||
DerivationTask task = new DerivationTask(activity, key -> {
|
||||
KeyDerivationTask task = new KeyDerivationTask(activity, (passSlot, key) -> {
|
||||
Cipher cipher;
|
||||
try {
|
||||
cipher = Slot.createEncryptCipher(key);
|
||||
|
@ -135,7 +138,7 @@ public class Dialogs {
|
|||
listener.onSlotResult(slot, cipher);
|
||||
dialog.dismiss();
|
||||
});
|
||||
task.execute(new DerivationTask.Params(slot, password));
|
||||
task.execute(new KeyDerivationTask.Params(slot, password));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -239,6 +242,57 @@ public class Dialogs {
|
|||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, Exception e) {
|
||||
showErrorDialog(context, message, e, null);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, CharSequence error) {
|
||||
showErrorDialog(context, message, error, null);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, Exception e, DialogInterface.OnClickListener listener) {
|
||||
showErrorDialog(context, message, e.toString(), listener);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Context context, @StringRes int message, CharSequence error, DialogInterface.OnClickListener listener) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_error, null);
|
||||
TextView textDetails = view.findViewById(R.id.error_details);
|
||||
textDetails.setText(error);
|
||||
TextView textMessage = view.findViewById(R.id.error_message);
|
||||
textMessage.setText(message);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog1, which);
|
||||
}
|
||||
})
|
||||
.setNeutralButton(R.string.details, (dialog1, which) -> {
|
||||
textDetails.setVisibility(View.VISIBLE);
|
||||
})
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button button = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
button.setOnClickListener(v -> {
|
||||
if (textDetails.getVisibility() == View.GONE) {
|
||||
textDetails.setVisibility(View.VISIBLE);
|
||||
button.setText(R.string.copy);
|
||||
} else {
|
||||
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (clipboard != null) {
|
||||
ClipData clip = ClipData.newPlainText("text/plain", error);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public interface NumberInputListener {
|
||||
void onNumberInputResult(int number);
|
||||
}
|
||||
|
|
|
@ -165,7 +165,7 @@ public class EditEntryActivity extends AegisActivity {
|
|||
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
|
||||
_rowCounter.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", info.getClass()));
|
||||
}
|
||||
_textDigits.setText(Integer.toString(info.getDigits()));
|
||||
|
||||
|
@ -204,7 +204,7 @@ public class EditEntryActivity extends AegisActivity {
|
|||
_rowCounter.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -510,7 +510,7 @@ public class EditEntryActivity extends AegisActivity {
|
|||
info = new HotpInfo(secret, algo, digits, counter);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
|
||||
info.setDigits(digits);
|
||||
|
|
|
@ -1,54 +1,40 @@
|
|||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticatedSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide;
|
||||
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.github.appintro.AppIntro2;
|
||||
import com.github.appintro.AppIntroFragment;
|
||||
import com.github.appintro.model.SliderPage;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class IntroActivity extends AppIntro2 implements DerivationTask.Callback {
|
||||
public static final int RESULT_OK = 0;
|
||||
public static final int RESULT_EXCEPTION = 1;
|
||||
|
||||
public class IntroActivity extends AppIntro2 {
|
||||
private CustomAuthenticatedSlide _authenticatedSlide;
|
||||
private CustomAuthenticationSlide _authenticationSlide;
|
||||
private Fragment _endSlide;
|
||||
|
||||
private Vault _vault;
|
||||
private VaultFile _vaultFile;
|
||||
private PasswordSlot _passwordSlot;
|
||||
private Cipher _passwordCipher;
|
||||
|
||||
private AegisApplication _app;
|
||||
private Preferences _prefs;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
_app = (AegisApplication) getApplication();
|
||||
// set FLAG_SECURE on the window of every IntroActivity
|
||||
_prefs = new Preferences(this);
|
||||
if (_prefs.isSecureScreenEnabled()) {
|
||||
|
@ -84,29 +70,13 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
|
|||
endSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
|
||||
_endSlide = AppIntroFragment.newInstance(endSliderPage);
|
||||
addSlide(_endSlide);
|
||||
|
||||
_vault = new Vault();
|
||||
_vaultFile = new VaultFile();
|
||||
}
|
||||
|
||||
private void setException(Exception e) {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("exception", e);
|
||||
setResult(RESULT_EXCEPTION, result);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlideChanged(Fragment oldFragment, Fragment newFragment) {
|
||||
Intent intent = getIntent();
|
||||
int cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
|
||||
|
||||
if (newFragment == _endSlide && cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
_passwordSlot = new PasswordSlot();
|
||||
DerivationTask.Params params = new DerivationTask.Params(_passwordSlot, _authenticatedSlide.getPassword());
|
||||
new DerivationTask(this, this).execute(params);
|
||||
} else if (oldFragment == _authenticationSlide && newFragment != _endSlide) {
|
||||
if (oldFragment == _authenticationSlide && newFragment != _endSlide) {
|
||||
// skip to the last slide if no encryption will be used
|
||||
int cryptType = getIntent().getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
// TODO: no magic indices
|
||||
goToNextSlide(false);
|
||||
|
@ -121,76 +91,35 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
|
|||
super.onDonePressed(currentFragment);
|
||||
|
||||
int cryptType = _authenticatedSlide.getCryptType();
|
||||
// wait for the key derivation background task
|
||||
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE &&
|
||||
(_passwordSlot == null || _passwordCipher == null)) {
|
||||
return;
|
||||
}
|
||||
VaultFileCredentials creds = _authenticatedSlide.getCredentials();
|
||||
|
||||
// generate the master key
|
||||
VaultFileCredentials creds = null;
|
||||
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
creds = new VaultFileCredentials();
|
||||
}
|
||||
|
||||
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
// encrypt the master key with a key derived from the user's password
|
||||
// and add it to the list of slots
|
||||
if (_passwordSlot == null || _passwordCipher == null) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
Vault vault = new Vault();
|
||||
VaultFile vaultFile = new VaultFile();
|
||||
try {
|
||||
_passwordSlot.setKey(creds.getKey(), _passwordCipher);
|
||||
creds.getSlots().add(_passwordSlot);
|
||||
} catch (SlotException e) {
|
||||
setException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
|
||||
BiometricSlot slot = _authenticatedSlide.getBiometricSlot();
|
||||
try {
|
||||
slot.setKey(creds.getKey(), _authenticatedSlide.getBiometriCipher());
|
||||
} catch (SlotException e) {
|
||||
setException(e);
|
||||
}
|
||||
creds.getSlots().add(slot);
|
||||
}
|
||||
|
||||
// finally, save the vault
|
||||
try {
|
||||
JSONObject obj = _vault.toJson();
|
||||
JSONObject obj = vault.toJson();
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
_vaultFile.setContent(obj);
|
||||
vaultFile.setContent(obj);
|
||||
} else {
|
||||
_vaultFile.setContent(obj, creds);
|
||||
vaultFile.setContent(obj, creds);
|
||||
}
|
||||
VaultManager.save(getApplicationContext(), _vaultFile);
|
||||
|
||||
VaultManager.save(getApplicationContext(), vaultFile);
|
||||
} catch (VaultManagerException | VaultFileException e) {
|
||||
setException(e);
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_init_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// send the master key back to the main activity
|
||||
Intent result = new Intent();
|
||||
result.putExtra("creds", creds);
|
||||
setResult(RESULT_OK, result);
|
||||
_app.initVaultManager(vault, creds);
|
||||
|
||||
// skip the intro from now on
|
||||
_prefs.setIntroDone(true);
|
||||
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskFinished(SecretKey key) {
|
||||
if (key != null) {
|
||||
try {
|
||||
_passwordCipher = Slot.createEncryptCipher(key);
|
||||
} catch (SlotException e) {
|
||||
setException(e);
|
||||
}
|
||||
} else {
|
||||
setException(new NullPointerException());
|
||||
}
|
||||
public void goToNextSlide() {
|
||||
super.goToNextSlide(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
|||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryListView;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
||||
|
@ -160,36 +160,36 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (data == null) {
|
||||
// don't process any activity results if the vault is locked
|
||||
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _app.isVaultLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't process any activity results if the vault is locked
|
||||
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _vault.isLocked()) {
|
||||
if (resultCode != RESULT_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case CODE_SCAN:
|
||||
onScanResult(resultCode, data);
|
||||
onScanResult(data);
|
||||
break;
|
||||
case CODE_ADD_ENTRY:
|
||||
onAddEntryResult(resultCode, data);
|
||||
onAddEntryResult(data);
|
||||
break;
|
||||
case CODE_EDIT_ENTRY:
|
||||
onEditEntryResult(resultCode, data);
|
||||
onEditEntryResult(data);
|
||||
break;
|
||||
case CODE_DO_INTRO:
|
||||
onDoIntroResult(resultCode, data);
|
||||
onDoIntroResult();
|
||||
break;
|
||||
case CODE_DECRYPT:
|
||||
onDecryptResult(resultCode, data);
|
||||
onDecryptResult();
|
||||
break;
|
||||
case CODE_PREFERENCES:
|
||||
onPreferencesResult(resultCode, data);
|
||||
onPreferencesResult(data);
|
||||
break;
|
||||
case CODE_SCAN_IMAGE:
|
||||
onScanImageResult(resultCode, data);
|
||||
onScanImageResult(data);
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
@ -212,7 +212,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
}
|
||||
}
|
||||
|
||||
private void onPreferencesResult(int resultCode, Intent data) {
|
||||
private void onPreferencesResult(Intent data) {
|
||||
// refresh the entire entry list if needed
|
||||
if (data.getBooleanExtra("needsRecreate", false)) {
|
||||
recreate();
|
||||
|
@ -246,23 +246,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
private void onScanResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
private void onScanResult(Intent data) {
|
||||
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAddEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
private void onAddEntryResult(Intent data) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
private void onEditEntryResult(Intent data) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
|
@ -272,10 +267,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
_entryListView.replaceEntry(entryUUID, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onScanImageResult(int resultCode, Intent intent) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
private void onScanImageResult(Intent intent) {
|
||||
Uri inputFile = (intent.getData());
|
||||
Bitmap bitmap;
|
||||
|
||||
|
@ -300,9 +293,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
|
||||
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
|
||||
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -348,28 +340,14 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
_entryListView.setGroupFilter(group, true);
|
||||
}
|
||||
|
||||
private void onDoIntroResult(int resultCode, Intent data) {
|
||||
if (resultCode == IntroActivity.RESULT_EXCEPTION) {
|
||||
// TODO: user feedback
|
||||
Exception e = (Exception) data.getSerializableExtra("exception");
|
||||
throw new RuntimeException(e);
|
||||
private void onDoIntroResult() {
|
||||
_vault = _app.getVaultManager();
|
||||
loadEntries();
|
||||
}
|
||||
|
||||
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
|
||||
unlockVault(creds);
|
||||
}
|
||||
|
||||
private void onDecryptResult(int resultCode, Intent intent) {
|
||||
VaultFileCredentials creds = (VaultFileCredentials) intent.getSerializableExtra("creds");
|
||||
boolean unlocked = unlockVault(creds);
|
||||
|
||||
// save the vault in case a slot was repaired
|
||||
if (unlocked && intent.getBooleanExtra("repairedSlot", false)) {
|
||||
_vault.setCredentials(creds);
|
||||
saveVault();
|
||||
}
|
||||
|
||||
doShortcutActions();
|
||||
private void onDecryptResult() {
|
||||
_vault = _app.getVaultManager();
|
||||
loadEntries();
|
||||
}
|
||||
|
||||
private void startScanActivity() {
|
||||
|
@ -392,7 +370,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
private void doShortcutActions() {
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getStringExtra("action");
|
||||
if (action == null || _vault.isLocked()) {
|
||||
if (action == null || _app.isVaultLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -406,7 +384,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
}
|
||||
|
||||
private void handleDeeplink() {
|
||||
if (_vault.isLocked()) {
|
||||
if (_app.isVaultLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -420,8 +398,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
try {
|
||||
info = GoogleAuthInfo.parseUri(uri);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
|
||||
}
|
||||
|
||||
if (info != null) {
|
||||
|
@ -435,19 +413,33 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (_vault.isLocked()) {
|
||||
if (_vault == null) {
|
||||
// start the intro if the vault file doesn't exist
|
||||
if (!_vault.isLoaded() && !_vault.fileExists()) {
|
||||
// the vault doesn't exist, start the intro
|
||||
if (!VaultManager.fileExists(this)) {
|
||||
if (getPreferences().isIntroDone()) {
|
||||
Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
Intent intro = new Intent(this, IntroActivity.class);
|
||||
startActivityForResult(intro, CODE_DO_INTRO);
|
||||
return;
|
||||
} else {
|
||||
unlockVault(null);
|
||||
}
|
||||
|
||||
// read the vault from disk
|
||||
// if this fails, show the error to the user and close the app
|
||||
try {
|
||||
VaultFile vaultFile = _app.loadVaultFile();
|
||||
if (!vaultFile.isEncrypted()) {
|
||||
_vault = _app.initVaultManager(vaultFile, null);
|
||||
}
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog1, which) -> finish());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_app.isVaultLocked()) {
|
||||
startAuthActivity();
|
||||
} else if (_loaded) {
|
||||
// update the list of groups in the filter menu
|
||||
if (_menu != null) {
|
||||
|
@ -599,32 +591,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
_searchView.setIconified(true);
|
||||
}
|
||||
|
||||
private boolean unlockVault(VaultFileCredentials creds) {
|
||||
try {
|
||||
if (!_vault.isLoaded()) {
|
||||
_vault.load();
|
||||
}
|
||||
if (_vault.isLocked()) {
|
||||
if (creds == null) {
|
||||
startAuthActivity();
|
||||
return false;
|
||||
} else {
|
||||
_vault.unlock(creds);
|
||||
}
|
||||
}
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(this, getString(R.string.decryption_error), Toast.LENGTH_LONG).show();
|
||||
startAuthActivity();
|
||||
return false;
|
||||
}
|
||||
|
||||
loadEntries();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void loadEntries() {
|
||||
// load all entries
|
||||
List<VaultEntry> entries = new ArrayList<VaultEntry>(_vault.getEntries());
|
||||
List<VaultEntry> entries = new ArrayList<>(_vault.getEntries());
|
||||
_entryListView.addEntries(entries);
|
||||
_entryListView.runEntriesAnimation();
|
||||
_loaded = true;
|
||||
|
@ -632,15 +600,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
|
||||
private void startAuthActivity() {
|
||||
Intent intent = new Intent(this, AuthActivity.class);
|
||||
intent.putExtra("slots", _vault.getFileHeader().getSlots());
|
||||
intent.putExtra("requiresUnlock", false);
|
||||
intent.putExtra("cancelAction", CancelAction.KILL);
|
||||
startActivityForResult(intent, CODE_DECRYPT);
|
||||
}
|
||||
|
||||
private void updateLockIcon() {
|
||||
// hide the lock icon if the vault is not unlocked
|
||||
if (_menu != null && !_vault.isLocked()) {
|
||||
if (_menu != null && !_app.isVaultLocked()) {
|
||||
MenuItem item = _menu.findItem(R.id.action_lock);
|
||||
item.setVisible(_vault.isEncryptionEnabled());
|
||||
}
|
||||
|
|
|
@ -287,7 +287,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
try {
|
||||
_vault.disableEncryption();
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(getActivity(), R.string.disable_encryption_error, Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.disable_encryption_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -404,7 +405,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
try {
|
||||
_vault.backup();
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(getContext(), String.format("Error creating backup: %s", e.getMessage()), Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.backup_error, e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
@ -553,7 +555,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -562,8 +565,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
}
|
||||
} catch (DatabaseImporterException e) {
|
||||
e.printStackTrace();
|
||||
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
|
||||
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.parsing_file_error, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -579,7 +581,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
state = ((AegisImporter.EncryptedState) _importerState).decrypt(creds);
|
||||
} catch (VaultFileException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -601,7 +603,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
Toast.makeText(getActivity(), R.string.file_not_found, Toast.LENGTH_SHORT).show();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(getActivity(), R.string.reading_file_error, Toast.LENGTH_SHORT).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.reading_file_error, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -611,8 +613,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
result = state.convert();
|
||||
} catch (DatabaseImporterException e) {
|
||||
e.printStackTrace();
|
||||
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
|
||||
Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.parsing_file_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -722,7 +723,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
try (OutputStream stream = getContext().getContentResolver().openOutputStream(uri, "w")) {
|
||||
_vault.export(stream, encrypt);
|
||||
} catch (IOException | VaultManagerException e) {
|
||||
Toast.makeText(getActivity(), R.string.exporting_vault_error, Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -748,7 +750,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
try {
|
||||
_vault.save();
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(getActivity(), R.string.saving_error, Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.saving_error, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -832,8 +835,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
e.printStackTrace();
|
||||
updateEncryptionPreferences();
|
||||
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -844,6 +848,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
try {
|
||||
slot.setKey(creds.getKey(), cipher);
|
||||
} catch (SlotException e) {
|
||||
e.printStackTrace();
|
||||
onSlotInitializationFailed(0, e.toString());
|
||||
return;
|
||||
}
|
||||
|
@ -857,7 +862,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
@Override
|
||||
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
|
||||
if (!BiometricsHelper.isCanceled(errorCode)) {
|
||||
Toast.makeText(getActivity(), String.format("%s: %s", getString(R.string.encryption_enable_biometrics_error), errString), Toast.LENGTH_LONG).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.encryption_enable_biometrics_error, errString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -882,7 +887,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,7 +107,6 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
|||
@Override
|
||||
public void handleResult(Result rawResult) {
|
||||
try {
|
||||
// parse google auth uri
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(rawResult.getText().trim());
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
|
||||
|
@ -116,10 +115,11 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
|||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Toast.makeText(this, getString(R.string.read_qr_error), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.read_qr_error, e, (dialog, which) -> {
|
||||
_scannerView.resumeCameraPreview(this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCameraIcon() {
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
@ -18,22 +17,29 @@ import com.beemdevelopment.aegis.R;
|
|||
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.IntroActivity;
|
||||
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.github.appintro.SlidePolicy;
|
||||
import com.github.appintro.SlideSelectionListener;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, SlideSelectionListener {
|
||||
private int _cryptType;
|
||||
private int _bgColor;
|
||||
private EditText _textPassword;
|
||||
private EditText _textPasswordConfirm;
|
||||
private CheckBox _checkPasswordVisibility;
|
||||
private int _bgColor;
|
||||
|
||||
private BiometricSlot _bioSlot;
|
||||
private Cipher _bioCipher;
|
||||
private int _cryptType;
|
||||
private VaultFileCredentials _creds;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
@ -54,6 +60,7 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
|
|||
}
|
||||
});
|
||||
|
||||
_creds = new VaultFileCredentials();
|
||||
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
|
||||
return view;
|
||||
}
|
||||
|
@ -62,23 +69,15 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
|
|||
return _cryptType;
|
||||
}
|
||||
|
||||
public BiometricSlot getBiometricSlot() {
|
||||
return _bioSlot;
|
||||
}
|
||||
|
||||
public Cipher getBiometriCipher() {
|
||||
return _bioCipher;
|
||||
}
|
||||
|
||||
public char[] getPassword() {
|
||||
return EditTextHelper.getEditTextChars(_textPassword);
|
||||
public VaultFileCredentials getCredentials() {
|
||||
return _creds;
|
||||
}
|
||||
|
||||
public void setBgColor(int color) {
|
||||
_bgColor = color;
|
||||
}
|
||||
|
||||
public void showBiometricPrompt() {
|
||||
private void showBiometricPrompt() {
|
||||
BiometricSlotInitializer initializer = new BiometricSlotInitializer(this, new BiometricsListener());
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.set_up_biometric))
|
||||
|
@ -87,14 +86,16 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
|
|||
initializer.authenticate(info);
|
||||
}
|
||||
|
||||
private void deriveKey() {
|
||||
PasswordSlot slot = new PasswordSlot();
|
||||
KeyDerivationTask.Params params = new KeyDerivationTask.Params(slot, EditTextHelper.getEditTextChars(_textPassword));
|
||||
new KeyDerivationTask(getContext(), new PasswordDerivationListener()).execute(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlideSelected() {
|
||||
Intent intent = getActivity().getIntent();
|
||||
_cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
|
||||
|
||||
if (_cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
|
||||
showBiometricPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -108,13 +109,13 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
|
|||
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
|
||||
return true;
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC:
|
||||
if (_bioSlot == null) {
|
||||
if (!_creds.getSlots().has(BiometricSlot.class)) {
|
||||
return false;
|
||||
}
|
||||
// intentional fallthrough
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
|
||||
if (EditTextHelper.getEditTextChars(_textPassword).length > 0) {
|
||||
return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm);
|
||||
if (EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
|
||||
return _creds.getSlots().has(PasswordSlot.class);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -134,23 +135,49 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
|
|||
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
|
||||
snackbar.show();
|
||||
}
|
||||
} else if (_bioSlot == null) {
|
||||
} else if (_cryptType != CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
|
||||
deriveKey();
|
||||
} else if (!_creds.getSlots().has(BiometricSlot.class)) {
|
||||
showBiometricPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
private class BiometricsListener implements BiometricSlotInitializer.Listener {
|
||||
private class PasswordDerivationListener implements KeyDerivationTask.Callback {
|
||||
@Override
|
||||
public void onTaskFinished(PasswordSlot slot, SecretKey key) {
|
||||
try {
|
||||
Cipher cipher = Slot.createEncryptCipher(key);
|
||||
slot.setKey(_creds.getKey(), cipher);
|
||||
_creds.getSlots().add(slot);
|
||||
} catch (SlotException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.enable_encryption_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
((IntroActivity) getActivity()).goToNextSlide();
|
||||
}
|
||||
}
|
||||
|
||||
private class BiometricsListener implements BiometricSlotInitializer.Listener {
|
||||
@Override
|
||||
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
|
||||
_bioSlot = slot;
|
||||
_bioCipher = cipher;
|
||||
try {
|
||||
slot.setKey(_creds.getKey(), cipher);
|
||||
_creds.getSlots().add(slot);
|
||||
} catch (SlotException e) {
|
||||
e.printStackTrace();
|
||||
onSlotInitializationFailed(0, e.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
deriveKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
|
||||
if (!BiometricsHelper.isCanceled(errorCode)) {
|
||||
Toast.makeText(CustomAuthenticatedSlide.this.getContext(), errString, Toast.LENGTH_LONG).show();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.encryption_enable_biometrics_error, errString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ public class CustomAuthenticationSlide extends Fragment implements SlidePolicy,
|
|||
id = CRYPT_TYPE_BIOMETRIC;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported security setting: %d", i));
|
||||
}
|
||||
|
||||
Intent intent = getActivity().getIntent();
|
||||
|
|
|
@ -9,16 +9,16 @@ import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
|||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, SecretKey> {
|
||||
public class KeyDerivationTask extends ProgressDialogTask<KeyDerivationTask.Params, KeyDerivationTask.Result> {
|
||||
private Callback _cb;
|
||||
|
||||
public DerivationTask(Context context, Callback cb) {
|
||||
public KeyDerivationTask(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.encrypting_vault));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SecretKey doInBackground(DerivationTask.Params... args) {
|
||||
protected Result doInBackground(KeyDerivationTask.Params... args) {
|
||||
setPriority();
|
||||
|
||||
Params params = args[0];
|
||||
|
@ -29,13 +29,16 @@ public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, Se
|
|||
CryptoUtils.CRYPTO_SCRYPT_p,
|
||||
salt
|
||||
);
|
||||
return params.getSlot().deriveKey(params.getPassword(), scryptParams);
|
||||
|
||||
PasswordSlot slot = params.getSlot();
|
||||
SecretKey key = slot.deriveKey(params.getPassword(), scryptParams);
|
||||
return new Result(slot, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(SecretKey key) {
|
||||
super.onPostExecute(key);
|
||||
_cb.onTaskFinished(key);
|
||||
protected void onPostExecute(Result result) {
|
||||
super.onPostExecute(result);
|
||||
_cb.onTaskFinished(result.getSlot(), result.getKey());
|
||||
}
|
||||
|
||||
public static class Params {
|
||||
|
@ -56,7 +59,25 @@ public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, Se
|
|||
}
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
private PasswordSlot _slot;
|
||||
private SecretKey _key;
|
||||
|
||||
public Result(PasswordSlot slot, SecretKey key) {
|
||||
_slot = slot;
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public PasswordSlot getSlot() {
|
||||
return _slot;
|
||||
}
|
||||
|
||||
public SecretKey getKey() {
|
||||
return _key;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(SecretKey key);
|
||||
void onTaskFinished(PasswordSlot slot, SecretKey key);
|
||||
}
|
||||
}
|
|
@ -5,41 +5,32 @@ import android.content.Context;
|
|||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTask.Params, SlotListTask.Result> {
|
||||
public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecryptTask.Params, PasswordSlotDecryptTask.Result> {
|
||||
private Callback _cb;
|
||||
private Class<T> _type;
|
||||
|
||||
public SlotListTask(Class<T> type, Context context, Callback cb) {
|
||||
public PasswordSlotDecryptTask(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.unlocking_vault));
|
||||
_cb = cb;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result doInBackground(SlotListTask.Params... args) {
|
||||
protected Result doInBackground(PasswordSlotDecryptTask.Params... args) {
|
||||
setPriority();
|
||||
|
||||
Params params = args[0];
|
||||
SlotList slots = params.getSlots();
|
||||
|
||||
for (Slot slot : slots.findAll(_type)) {
|
||||
for (PasswordSlot slot : params.getSlots()) {
|
||||
try {
|
||||
if (slot instanceof PasswordSlot) {
|
||||
char[] password = (char[]) params.getObj();
|
||||
return decryptPasswordSlot((PasswordSlot) slot, password);
|
||||
} else if (slot instanceof BiometricSlot) {
|
||||
return decryptBiometricSlot((BiometricSlot) slot, (Cipher) params.getObj());
|
||||
}
|
||||
return decryptPasswordSlot(slot, params.getPassword());
|
||||
} catch (SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (SlotIntegrityException ignored) {
|
||||
|
@ -50,12 +41,6 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
|
|||
return null;
|
||||
}
|
||||
|
||||
private Result decryptBiometricSlot(BiometricSlot slot, Cipher cipher)
|
||||
throws SlotException, SlotIntegrityException {
|
||||
MasterKey key = slot.getKey(cipher);
|
||||
return new Result(key, slot);
|
||||
}
|
||||
|
||||
private Result decryptPasswordSlot(PasswordSlot slot, char[] password)
|
||||
throws SlotIntegrityException, SlotException {
|
||||
MasterKey masterKey;
|
||||
|
@ -103,35 +88,35 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
|
|||
}
|
||||
|
||||
public static class Params {
|
||||
private SlotList _slots;
|
||||
private Object _obj;
|
||||
private List<PasswordSlot> _slots;
|
||||
private char[] _password;
|
||||
|
||||
public Params(SlotList slots, Object obj) {
|
||||
public Params(List<PasswordSlot> slots, char[] password) {
|
||||
_slots = slots;
|
||||
_obj = obj;
|
||||
_password = password;
|
||||
}
|
||||
|
||||
public SlotList getSlots() {
|
||||
public List<PasswordSlot> getSlots() {
|
||||
return _slots;
|
||||
}
|
||||
|
||||
public Object getObj() {
|
||||
return _obj;
|
||||
public char[] getPassword() {
|
||||
return _password;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
private MasterKey _key;
|
||||
private Slot _slot;
|
||||
private PasswordSlot _slot;
|
||||
private boolean _repaired;
|
||||
|
||||
public Result(MasterKey key, Slot slot, boolean repaired) {
|
||||
public Result(MasterKey key, PasswordSlot slot, boolean repaired) {
|
||||
_key = key;
|
||||
_slot = slot;
|
||||
_repaired = repaired;
|
||||
}
|
||||
|
||||
public Result(MasterKey key, Slot slot) {
|
||||
public Result(MasterKey key, PasswordSlot slot) {
|
||||
this(key, slot, false);
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ public class SlotHolder extends RecyclerView.ViewHolder {
|
|||
_slotName.setText(R.string.authentication_method_raw);
|
||||
_slotImg.setImageResource(R.drawable.ic_vpn_key_black_24dp);
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
throw new RuntimeException(String.format("Unsupported Slot type: %s", slot.getClass()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.core.util.AtomicFile;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
@ -25,59 +23,60 @@ public class VaultManager {
|
|||
public static final String FILENAME_EXPORT_PLAIN = "aegis-export-plain.json";
|
||||
|
||||
private Vault _vault;
|
||||
private VaultFile _file;
|
||||
private VaultFileCredentials _creds;
|
||||
private boolean _encrypt;
|
||||
|
||||
private Context _context;
|
||||
private Preferences _prefs;
|
||||
private VaultBackupManager _backups;
|
||||
|
||||
public VaultManager(Context context) {
|
||||
public VaultManager(Context context, Vault vault, VaultFileCredentials creds) {
|
||||
_context = context;
|
||||
_prefs = new Preferences(context);
|
||||
_backups = new VaultBackupManager(context);
|
||||
_vault = vault;
|
||||
_creds = creds;
|
||||
}
|
||||
|
||||
public boolean fileExists() {
|
||||
File file = new File(_context.getFilesDir(), FILENAME);
|
||||
public VaultManager(Context context, Vault vault) {
|
||||
this(context, vault, null);
|
||||
}
|
||||
|
||||
public static boolean fileExists(Context context) {
|
||||
File file = new File(context.getFilesDir(), FILENAME);
|
||||
return file.exists() && file.isFile();
|
||||
}
|
||||
|
||||
public void load() throws VaultManagerException {
|
||||
assertState(true, false);
|
||||
public static VaultFile readFile(Context context) throws VaultManagerException {
|
||||
AtomicFile file = new AtomicFile(new File(context.getFilesDir(), FILENAME));
|
||||
|
||||
AtomicFile file = new AtomicFile(new File(_context.getFilesDir(), FILENAME));
|
||||
try {
|
||||
byte[] fileBytes = file.readFully();
|
||||
_file = VaultFile.fromBytes(fileBytes);
|
||||
_encrypt = _file.isEncrypted();
|
||||
if (!isEncryptionEnabled()) {
|
||||
JSONObject obj = _file.getContent();
|
||||
_vault = Vault.fromJson(obj);
|
||||
}
|
||||
} catch (IOException | VaultFileException | VaultException e) {
|
||||
return VaultFile.fromBytes(fileBytes);
|
||||
} catch (IOException | VaultFileException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void lock() {
|
||||
assertState(false, true);
|
||||
_creds = null;
|
||||
_vault = null;
|
||||
public static VaultManager init(Context context, VaultFile file, VaultFileCredentials creds) throws VaultManagerException {
|
||||
if (file.isEncrypted() && creds == null) {
|
||||
throw new IllegalArgumentException("The VaultFile is encrypted but the given VaultFileCredentials is null");
|
||||
}
|
||||
|
||||
public void unlock(VaultFileCredentials creds) throws VaultManagerException {
|
||||
assertState(true, true);
|
||||
|
||||
Vault vault;
|
||||
try {
|
||||
JSONObject obj = _file.getContent(creds);
|
||||
_vault = Vault.fromJson(obj);
|
||||
_creds = creds;
|
||||
_context.startService(new Intent(_context, NotificationService.class));
|
||||
} catch (VaultFileException | VaultException e) {
|
||||
JSONObject obj;
|
||||
if (!file.isEncrypted()) {
|
||||
obj = file.getContent();
|
||||
} else {
|
||||
obj = file.getContent(creds);
|
||||
}
|
||||
|
||||
vault = Vault.fromJson(obj);
|
||||
} catch (VaultException | VaultFileException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
|
||||
return new VaultManager(context, vault, creds);
|
||||
}
|
||||
|
||||
public static void save(Context context, VaultFile vaultFile) throws VaultManagerException {
|
||||
|
@ -98,16 +97,17 @@ public class VaultManager {
|
|||
}
|
||||
|
||||
public void save() throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
|
||||
try {
|
||||
JSONObject obj = _vault.toJson();
|
||||
|
||||
VaultFile file = new VaultFile();
|
||||
if (isEncryptionEnabled()) {
|
||||
_file.setContent(obj, _creds);
|
||||
file.setContent(obj, _creds);
|
||||
} else {
|
||||
_file.setContent(obj);
|
||||
file.setContent(obj);
|
||||
}
|
||||
save(_context, _file);
|
||||
|
||||
save(_context, file);
|
||||
|
||||
if (_prefs.isBackupsEnabled()) {
|
||||
backup();
|
||||
|
@ -118,8 +118,6 @@ public class VaultManager {
|
|||
}
|
||||
|
||||
public void export(OutputStream stream, boolean encrypt) throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
|
||||
try {
|
||||
VaultFile vaultFile = new VaultFile();
|
||||
if (encrypt && isEncryptionEnabled()) {
|
||||
|
@ -136,48 +134,38 @@ public class VaultManager {
|
|||
}
|
||||
|
||||
public void backup() throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
_backups.create(_prefs.getBackupsLocation(), _prefs.getBackupsVersionCount());
|
||||
}
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
_vault.getEntries().add(entry);
|
||||
}
|
||||
|
||||
public VaultEntry getEntryByUUID(UUID uuid) {
|
||||
assertState(false, true);
|
||||
return _vault.getEntries().getByUUID(uuid);
|
||||
}
|
||||
|
||||
public VaultEntry removeEntry(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
return _vault.getEntries().remove(entry);
|
||||
}
|
||||
|
||||
public VaultEntry replaceEntry(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
return _vault.getEntries().replace(entry);
|
||||
}
|
||||
|
||||
public void swapEntries(VaultEntry entry1, VaultEntry entry2) {
|
||||
assertState(false, true);
|
||||
_vault.getEntries().swap(entry1, entry2);
|
||||
}
|
||||
|
||||
public boolean isEntryDuplicate(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
return _vault.getEntries().has(entry);
|
||||
}
|
||||
|
||||
public Collection<VaultEntry> getEntries() {
|
||||
assertState(false, true);
|
||||
return _vault.getEntries().getValues();
|
||||
}
|
||||
|
||||
public TreeSet<String> getGroups() {
|
||||
assertState(false, true);
|
||||
|
||||
TreeSet<String> groups = new TreeSet<>(Collator.getInstance());
|
||||
for (VaultEntry entry : getEntries()) {
|
||||
String group = entry.getGroup();
|
||||
|
@ -189,62 +177,24 @@ public class VaultManager {
|
|||
}
|
||||
|
||||
public VaultFileCredentials getCredentials() {
|
||||
assertState(false, true);
|
||||
return _creds;
|
||||
}
|
||||
|
||||
public void setCredentials(VaultFileCredentials creds) {
|
||||
assertState(false, true);
|
||||
_creds = creds;
|
||||
}
|
||||
|
||||
public VaultFile.Header getFileHeader() {
|
||||
assertLoaded(true);
|
||||
return _file.getHeader();
|
||||
}
|
||||
|
||||
public boolean isEncryptionEnabled() {
|
||||
assertLoaded(true);
|
||||
return _encrypt;
|
||||
return _creds != null;
|
||||
}
|
||||
|
||||
public void enableEncryption(VaultFileCredentials creds) throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
_creds = creds;
|
||||
_encrypt = true;
|
||||
save();
|
||||
}
|
||||
|
||||
public void disableEncryption() throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
_creds = null;
|
||||
_encrypt = false;
|
||||
save();
|
||||
}
|
||||
|
||||
public boolean isLoaded() {
|
||||
return _file != null;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return _vault == null;
|
||||
}
|
||||
|
||||
private void assertState(boolean locked, boolean loaded) {
|
||||
assertLoaded(loaded);
|
||||
|
||||
if (isLocked() && !locked) {
|
||||
throw new AssertionError("vault file has not been unlocked yet");
|
||||
} else if (!isLocked() && locked) {
|
||||
throw new AssertionError("vault file has already been unlocked");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertLoaded(boolean loaded) {
|
||||
if (isLoaded() && !loaded) {
|
||||
throw new AssertionError("vault file has already been loaded");
|
||||
} else if (!isLoaded() && loaded) {
|
||||
throw new AssertionError("vault file has not been loaded yet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
21
app/src/main/res/layout/dialog_error.xml
Normal file
21
app/src/main/res/layout/dialog_error.xml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="25dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingEnd="25dp"
|
||||
android:paddingTop="10dp">
|
||||
<TextView
|
||||
android:id="@+id/error_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
<TextView
|
||||
android:id="@+id/error_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="10dp"
|
||||
android:textStyle="italic"/>
|
||||
</LinearLayout>
|
|
@ -126,12 +126,19 @@
|
|||
<string name="copied">Copied</string>
|
||||
<string name="errors_copied">Errors copied to the clipboard</string>
|
||||
<string name="version_copied">Version copied to the clipboard</string>
|
||||
<string name="error_occurred">An error occurred</string>
|
||||
<string name="decryption_error">An error occurred while trying to unlock the vault</string>
|
||||
<string name="decryption_corrupt_error">An error occurred while trying to unlock the vault. Your vault file might be corrupt.</string>
|
||||
<string name="saving_error">An error occurred while trying to save the vault</string>
|
||||
<string name="vault_init_error">An error occurred while trying to initialize the vault</string>
|
||||
<string name="vault_load_error">An error occurred while trying to load the vault from storage</string>
|
||||
<string name="biometric_decrypt_error">An error occurred while trying to decrypt the vault with biometric authentication. This usually only happens if the security settings of your device were changed. Please unlock the vault with your password and reconfigure biometric authentication in the settings of Aegis.</string>
|
||||
<string name="biometric_init_error">An error occurred while trying to prepare biometric authentication. This usually only happens if the security settings of your device were changed. Please unlock the vault with your password and reconfigure biometric authentication in the settings of Aegis.</string>
|
||||
<string name="disable_encryption">Disable encryption</string>
|
||||
<string name="disable_encryption_description">Are you sure you want to disable encryption? This will cause the vault to be stored in plain text.</string>
|
||||
<string name="enable_encryption_error">An error occurred while enabling encryption</string>
|
||||
<string name="disable_encryption_error">An error occurred while disabling encryption</string>
|
||||
<string name="backup_error">An error occurred while trying to create a backup</string>
|
||||
<string name="permission_denied">Permission denied</string>
|
||||
<string name="andotp_new_format">New format (v0.6.3 or newer) </string>
|
||||
<string name="andotp_old_format">Old format (v0.6.2 or older) </string>
|
||||
|
@ -151,8 +158,8 @@
|
|||
<string name="exporting_vault_error">An error occurred while trying to export the vault</string>
|
||||
<string name="exported_vault">The vault has been exported</string>
|
||||
<string name="export_warning">This action will export the vault out of Aegis\' private storage.</string>
|
||||
<string name="encryption_set_password_error">An error occurred while trying to set the password: </string>
|
||||
<string name="encryption_enable_biometrics_error">An error occurred while trying to enable biometric unlock</string>
|
||||
<string name="encryption_set_password_error">An error occurred while trying to set the password.</string>
|
||||
<string name="encryption_enable_biometrics_error">An error occurred while trying to enable biometric unlock. Some devices have poor implementations of biometric authentication and it is likely that yours is one of them. Consider switching to a password-only configuration instead.</string>
|
||||
<string name="no_cameras_available">No cameras available</string>
|
||||
<string name="read_qr_error">An error occurred while trying to read the QR code</string>
|
||||
<string name="authentication_method_raw">Raw</string>
|
||||
|
|
Loading…
Add table
Reference in a new issue