Merge pull request #378 from alexbakker/better-errors

Improve overall exception handling and error feedback to the user
This commit is contained in:
Michael Schättgen 2020-05-09 17:16:33 +02:00 committed by GitHub
commit 8ff817856e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 544 additions and 465 deletions

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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() {

View file

@ -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

View file

@ -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);
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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());
}

View file

@ -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);
}
}
}

View file

@ -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() {

View file

@ -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);
}
}
}

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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()));
}
}

View file

@ -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");
}
}
}

View 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>

View file

@ -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>