mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-04 20:30:36 +00:00
Improve overall exception handling and error feedback to the user
The main goals of this patch are to: - Improve the exception handling in Aegis and the way we present errors messages to the user when they occur. - Write exception stack traces to the log in more places, so that the ADB logs we ask for from our users when reporting bugs become more useful. - Reduce the amount of times we throw a RuntimeException, particularly when an Android Keystore operation fails. Achieving the above goals ended up resulting in a very large refactor. The intro and unlock flow of the app need to be retested entirely.
This commit is contained in:
parent
499f930086
commit
cda78c56c5
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) {
|
||||
return true;
|
||||
}
|
||||
} 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) {
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
|
||||
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);
|
||||
}
|
||||
char[] password = EditTextHelper.getEditTextChars(_textPassword);
|
||||
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,26 +230,56 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskFinished(SlotListTask.Result result) {
|
||||
if (result != null) {
|
||||
// replace the old slot with the repaired one
|
||||
if (result.isSlotRepaired()) {
|
||||
_slots.replace(result.getSlot());
|
||||
}
|
||||
private void finish(MasterKey key, boolean isSlotRepaired) {
|
||||
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
|
||||
|
||||
if (result.getSlot().getType() == Slot.TYPE_DERIVED) {
|
||||
_prefs.resetPasswordReminderTimestamp();
|
||||
}
|
||||
|
||||
// send the master key back to the main activity
|
||||
if (_stateless) {
|
||||
// send the master key back to the calling activity
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("creds", new VaultFileCredentials(result.getKey(), _slots));
|
||||
intent.putExtra("repairedSlot", result.isSlotRepaired());
|
||||
intent.putExtra("creds", creds);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
} else {
|
||||
showError();
|
||||
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(PasswordSlotDecryptTask.Result result) {
|
||||
if (result != null) {
|
||||
// replace the old slot with the repaired one
|
||||
if (result.isSlotRepaired()) {
|
||||
_slots.replace(result.getSlot());
|
||||
}
|
||||
|
||||
if (result.getSlot().getType() == Slot.TYPE_DERIVED) {
|
||||
_prefs.resetPasswordReminderTimestamp();
|
||||
}
|
||||
|
||||
finish(result.getKey(), result.isSlotRepaired());
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
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
|
||||
Vault vault = new Vault();
|
||||
VaultFile vaultFile = new VaultFile();
|
||||
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,63 +246,55 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
private void onScanResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
|
||||
}
|
||||
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) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
private void onAddEntryResult(Intent data) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
|
||||
private void onEditEntryResult(Intent data) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
_entryListView.removeEntry(entryUUID);
|
||||
} else {
|
||||
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
|
||||
_entryListView.addEntry(entry);
|
||||
_entryListView.replaceEntry(entryUUID, entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
private void onScanImageResult(Intent intent) {
|
||||
Uri inputFile = (intent.getData());
|
||||
Bitmap bitmap;
|
||||
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
_entryListView.removeEntry(entryUUID);
|
||||
} else {
|
||||
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
|
||||
_entryListView.replaceEntry(entryUUID, entry);
|
||||
try {
|
||||
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
|
||||
|
||||
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
|
||||
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onScanImageResult(int resultCode, Intent intent) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
Uri inputFile = (intent.getData());
|
||||
Bitmap bitmap;
|
||||
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
|
||||
try {
|
||||
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
|
||||
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
|
||||
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
|
||||
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
|
||||
}
|
||||
Reader reader = new MultiFormatReader();
|
||||
Result result = reader.decode(binaryBitmap);
|
||||
|
||||
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
|
||||
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
|
||||
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
Reader reader = new MultiFormatReader();
|
||||
Result result = reader.decode(binaryBitmap);
|
||||
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
|
||||
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();
|
||||
}
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
|
||||
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
|
||||
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);
|
||||
}
|
||||
|
||||
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
|
||||
unlockVault(creds);
|
||||
private void onDoIntroResult() {
|
||||
_vault = _app.getVaultManager();
|
||||
loadEntries();
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
_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 void unlock(VaultFileCredentials creds) throws VaultManagerException {
|
||||
assertState(true, true);
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue