Make fingerprint unlock toggleable

Also:
- Fix a bug where setting the password would clear the key slot list
- Show the "Key slots" preference if multiple slots of one kind exist
This commit is contained in:
Alexander Bakker 2018-11-15 21:23:18 +01:00
parent 926b5139da
commit 80b1967693
10 changed files with 317 additions and 261 deletions

View file

@ -2,12 +2,7 @@ package me.impy.aegis.ui;
import android.os.Bundle;
import javax.crypto.Cipher;
import me.impy.aegis.db.slots.Slot;
import me.impy.aegis.ui.dialogs.PasswordDialogFragment;
public class PreferencesActivity extends AegisActivity implements PasswordDialogFragment.Listener {
public class PreferencesActivity extends AegisActivity {
private PreferencesFragment _fragment;
@Override
@ -41,14 +36,4 @@ public class PreferencesActivity extends AegisActivity implements PasswordDialog
outState.putParcelable("result", _fragment.getResult());
super.onSaveInstanceState(outState);
}
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
_fragment.onSlotResult(slot, cipher);
}
@Override
public void onException(Exception e) {
_fragment.onException(e);
}
}

View file

@ -29,20 +29,23 @@ import me.impy.aegis.BuildConfig;
import me.impy.aegis.R;
import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.db.DatabaseFileCredentials;
import me.impy.aegis.db.DatabaseFileException;
import me.impy.aegis.db.DatabaseManager;
import me.impy.aegis.db.DatabaseManagerException;
import me.impy.aegis.db.slots.FingerprintSlot;
import me.impy.aegis.db.slots.PasswordSlot;
import me.impy.aegis.db.slots.Slot;
import me.impy.aegis.db.slots.SlotException;
import me.impy.aegis.db.slots.SlotList;
import me.impy.aegis.helpers.FingerprintHelper;
import me.impy.aegis.helpers.PermissionHelper;
import me.impy.aegis.importers.AegisImporter;
import me.impy.aegis.importers.DatabaseImporter;
import me.impy.aegis.importers.DatabaseImporterException;
import me.impy.aegis.ui.dialogs.PasswordDialogFragment;
import me.impy.aegis.ui.dialogs.Dialogs;
import me.impy.aegis.ui.preferences.SwitchPreference;
import me.impy.aegis.util.ByteInputStream;
public class PreferencesFragment extends PreferenceFragmentCompat implements PasswordDialogFragment.Listener {
public class PreferencesFragment extends PreferenceFragmentCompat {
// activity request codes
private static final int CODE_IMPORT = 0;
private static final int CODE_IMPORT_DECRYPT = 1;
@ -61,6 +64,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
private Class<? extends DatabaseImporter> _importerType;
private SwitchPreference _encryptionPreference;
private SwitchPreference _fingerprintPreference;
private Preference _setPasswordPreference;
private Preference _slotsPreference;
@Override
@ -140,9 +145,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (!_db.isEncryptionEnabled()) {
PasswordDialogFragment dialog = new PasswordDialogFragment();
// TODO: find a less ugly way to obtain the fragment manager
dialog.show(getActivity().getSupportFragmentManager(), null);
Dialogs.showSetPasswordDialog(getActivity(), new EnableEncryptionListener());
} else {
new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.disable_encryption))
@ -154,7 +157,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
} catch (DatabaseManagerException e) {
Toast.makeText(getActivity(), getString(R.string.encrypting_error), Toast.LENGTH_SHORT).show();
}
updateEncryptionPreference();
updateEncryptionPreferences();
}
})
.setNegativeButton(android.R.string.no, null)
@ -164,20 +167,37 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
}
});
Preference setPasswordPreference = findPreference("pref_password");
setPasswordPreference.setOnPreferenceClickListener(preference -> {
PasswordDialogFragment dialog = new PasswordDialogFragment();
// TODO: find a less ugly way to obtain the fragment manager
dialog.show(getActivity().getSupportFragmentManager(), null);
_fingerprintPreference = (SwitchPreference) findPreference("pref_fingerprint");
_fingerprintPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
DatabaseFileCredentials creds = _db.getCredentials();
SlotList slots = creds.getSlots();
if (!slots.has(FingerprintSlot.class)) {
Dialogs.showFingerprintDialog(getActivity(), new RegisterFingerprintListener());
} else {
// remove the fingerprint slot
FingerprintSlot slot = slots.find(FingerprintSlot.class);
slots.remove(slot);
_db.setCredentials(creds);
saveDatabase();
updateEncryptionPreferences();
}
return false;
}
});
_setPasswordPreference = findPreference("pref_password");
_setPasswordPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showSetPasswordDialog(getActivity(), new SetPasswordListener());
return false;
});
_slotsPreference = findPreference("pref_slots");
if (BuildConfig.DEBUG) {
_slotsPreference.setVisible(true);
} else {
_slotsPreference.setVisible(false);
}
_slotsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
@ -187,7 +207,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
return true;
}
});
updateEncryptionPreference();
updateEncryptionPreferences();
}
@Override
@ -383,6 +404,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
_db.setCredentials(creds);
saveDatabase();
updateEncryptionPreferences();
}
private boolean saveDatabase() {
@ -396,31 +418,108 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
return true;
}
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = new DatabaseFileCredentials();
try {
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
_db.enableEncryption(creds);
} catch (DatabaseManagerException | SlotException e) {
onException(e);
return;
}
updateEncryptionPreference();
}
@Override
public void onException(Exception e) {
updateEncryptionPreference();
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
}
private void updateEncryptionPreference() {
private void updateEncryptionPreferences() {
boolean encrypted = _db.isEncryptionEnabled();
_encryptionPreference.setChecked(encrypted, true);
_setPasswordPreference.setVisible(encrypted);
_fingerprintPreference.setVisible(encrypted);
_slotsPreference.setEnabled(encrypted);
if (encrypted) {
SlotList slots = _db.getCredentials().getSlots();
boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1;
boolean multiFinger = slots.findAll(FingerprintSlot.class).size() > 1;
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiFinger;
_setPasswordPreference.setEnabled(!multiPassword);
_fingerprintPreference.setEnabled(FingerprintHelper.getManager(getContext()) != null && !multiFinger);
_fingerprintPreference.setChecked(slots.has(FingerprintSlot.class), true);
_slotsPreference.setVisible(showSlots);
} else {
_setPasswordPreference.setEnabled(false);
_fingerprintPreference.setEnabled(false);
_fingerprintPreference.setChecked(false, true);
_slotsPreference.setVisible(false);
}
}
private class SetPasswordListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = new DatabaseFileCredentials();
try {
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
_db.enableEncryption(creds);
} catch (DatabaseManagerException | SlotException e) {
onException(e);
return;
}
updateEncryptionPreferences();
}
@Override
public void onException(Exception e) {
updateEncryptionPreferences();
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
private class RegisterFingerprintListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = _db.getCredentials();
SlotList slots = creds.getSlots();
try {
slot.setKey(creds.getKey(), cipher);
} catch (SlotException e) {
onException(e);
return;
}
slots.add(slot);
_db.setCredentials(creds);
saveDatabase();
updateEncryptionPreferences();
}
@Override
public void onException(Exception e) {
Toast.makeText(getActivity(), getString(R.string.encryption_enable_fingerprint_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
private class EnableEncryptionListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = _db.getCredentials();
SlotList slots = creds.getSlots();
try {
// encrypt the master key for this slot
slot.setKey(creds.getKey(), cipher);
// remove the old master password slot
PasswordSlot oldSlot = creds.getSlots().find(PasswordSlot.class);
slots.remove(oldSlot);
// add the new master password slot
slots.add(slot);
} catch (SlotException e) {
onException(e);
return;
}
_db.setCredentials(creds);
saveDatabase();
}
@Override
public void onException(Exception e) {
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
}

View file

@ -24,11 +24,9 @@ import me.impy.aegis.db.slots.SlotList;
import me.impy.aegis.db.slots.SlotException;
import me.impy.aegis.helpers.FingerprintHelper;
import me.impy.aegis.ui.dialogs.Dialogs;
import me.impy.aegis.ui.dialogs.FingerprintDialogFragment;
import me.impy.aegis.ui.views.SlotAdapter;
import me.impy.aegis.ui.dialogs.SlotDialogFragment;
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, SlotDialogFragment.Listener {
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, Dialogs.SlotListener {
private DatabaseFileCredentials _creds;
private SlotAdapter _adapter;
@ -45,8 +43,11 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
bar.setDisplayHomeAsUpEnabled(true);
findViewById(R.id.button_add_fingerprint).setOnClickListener(view -> {
FingerprintDialogFragment dialog = new FingerprintDialogFragment();
dialog.show(getSupportFragmentManager(), null);
Dialogs.showFingerprintDialog(this ,this);
});
findViewById(R.id.button_add_password).setOnClickListener(view -> {
Dialogs.showSetPasswordDialog(this, this);
});
// set up the recycler view

View file

@ -2,17 +2,40 @@ package me.impy.aegis.ui.dialogs;
import android.app.Activity;
import android.content.DialogInterface;
import android.hardware.fingerprint.FingerprintManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.mattprecious.swirl.SwirlView;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import androidx.appcompat.app.AlertDialog;
import me.impy.aegis.R;
import me.impy.aegis.crypto.KeyStoreHandle;
import me.impy.aegis.crypto.KeyStoreHandleException;
import me.impy.aegis.db.slots.FingerprintSlot;
import me.impy.aegis.db.slots.PasswordSlot;
import me.impy.aegis.db.slots.Slot;
import me.impy.aegis.db.slots.SlotException;
import me.impy.aegis.helpers.EditTextHelper;
import me.impy.aegis.helpers.FingerprintHelper;
import me.impy.aegis.helpers.FingerprintUiHelper;
import me.impy.aegis.ui.tasks.DerivationTask;
public class Dialogs {
private Dialogs() {
}
public static AlertDialog showDeleteEntryDialog(Activity activity, DialogInterface.OnClickListener onDelete) {
return new AlertDialog.Builder(activity)
public static void showDeleteEntryDialog(Activity activity, DialogInterface.OnClickListener onDelete) {
new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.delete_entry))
.setMessage(activity.getString(R.string.delete_entry_description))
.setPositiveButton(android.R.string.yes, onDelete)
@ -20,12 +43,115 @@ public class Dialogs {
.show();
}
public static AlertDialog showDiscardDialog(Activity activity, DialogInterface.OnClickListener onSave, DialogInterface.OnClickListener onDiscard) {
return new AlertDialog.Builder(activity)
public static void showDiscardDialog(Activity activity, DialogInterface.OnClickListener onSave, DialogInterface.OnClickListener onDiscard) {
new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.discard_changes))
.setMessage(activity.getString(R.string.discard_changes_description))
.setPositiveButton(R.string.save, onSave)
.setNegativeButton(R.string.discard, onDiscard)
.show();
}
public static void showSetPasswordDialog(Activity activity, Dialogs.SlotListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null);
EditText textPassword = view.findViewById(R.id.text_password);
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
AlertDialog alert = new AlertDialog.Builder(activity)
.setTitle(R.string.set_password)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
final Button[] buttonOK = new Button[1];
alert.setOnShowListener(dialog -> {
buttonOK[0] = alert.getButton(AlertDialog.BUTTON_POSITIVE);
buttonOK[0].setEnabled(false);
// replace the default listener
buttonOK[0].setOnClickListener(v -> {
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm)) {
return;
}
char[] password = EditTextHelper.getEditTextChars(textPassword);
PasswordSlot slot = new PasswordSlot();
DerivationTask task = new DerivationTask(activity, key -> {
Cipher cipher;
try {
cipher = Slot.createEncryptCipher(key);
} catch (SlotException e) {
listener.onException(e);
dialog.cancel();
return;
}
listener.onSlotResult(slot, cipher);
dialog.dismiss();
});
task.execute(new DerivationTask.Params(slot, password));
});
});
TextWatcher watcher = new TextWatcher() {
public void onTextChanged(CharSequence c, int start, int before, int count) {
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
buttonOK[0].setEnabled(equal);
}
public void beforeTextChanged(CharSequence c, int start, int count, int after) { }
public void afterTextChanged(Editable c) { }
};
textPassword.addTextChangedListener(watcher);
textPasswordConfirm.addTextChangedListener(watcher);
alert.show();
}
public static void showFingerprintDialog(Activity activity, Dialogs.SlotListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_fingerprint, null);
TextView textFingerprint = view.findViewById(R.id.text_fingerprint);
SwirlView imgFingerprint = view.findViewById(R.id.img_fingerprint);
Cipher cipher;
FingerprintSlot slot;
final FingerprintUiHelper[] helper = new FingerprintUiHelper[1];
FingerprintManager manager = FingerprintHelper.getManager(activity);
try {
slot = new FingerprintSlot();
SecretKey key = new KeyStoreHandle().generateKey(slot.getUUID().toString());
cipher = Slot.createEncryptCipher(key);
} catch (KeyStoreHandleException | SlotException e) {
throw new RuntimeException(e);
}
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.register_fingerprint)
.setView(view)
.setNegativeButton(android.R.string.cancel, null)
.setOnDismissListener(d -> {
helper[0].stopListening();
})
.show();
helper[0] = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, new FingerprintUiHelper.Callback() {
@Override
public void onAuthenticated() {
listener.onSlotResult(slot, cipher);
dialog.dismiss();
}
@Override
public void onError() {
}
});
helper[0].startListening(new FingerprintManager.CryptoObject(cipher));
}
public interface SlotListener {
void onSlotResult(Slot slot, Cipher cipher);
void onException(Exception e);
}
}

View file

@ -1,82 +0,0 @@
package me.impy.aegis.ui.dialogs;
import android.app.Dialog;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.annotation.NonNull;
import android.view.View;
import android.widget.TextView;
import com.mattprecious.swirl.SwirlView;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import me.impy.aegis.R;
import me.impy.aegis.crypto.KeyStoreHandle;
import me.impy.aegis.crypto.KeyStoreHandleException;
import me.impy.aegis.db.slots.FingerprintSlot;
import me.impy.aegis.db.slots.Slot;
import me.impy.aegis.db.slots.SlotException;
import me.impy.aegis.helpers.FingerprintHelper;
import me.impy.aegis.helpers.FingerprintUiHelper;
public class FingerprintDialogFragment extends SlotDialogFragment implements FingerprintUiHelper.Callback {
private Cipher _cipher;
private FingerprintUiHelper _helper;
private FingerprintSlot _slot;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_fingerprint, null);
TextView textFingerprint = view.findViewById(R.id.text_fingerprint);
SwirlView imgFingerprint = view.findViewById(R.id.img_fingerprint);
FingerprintManager manager = FingerprintHelper.getManager(getContext());
try {
_slot = new FingerprintSlot();
SecretKey key = new KeyStoreHandle().generateKey(_slot.getUUID().toString());
_cipher = Slot.createEncryptCipher(key);
_helper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
} catch (KeyStoreHandleException | SlotException e) {
throw new RuntimeException(e);
}
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.register_fingerprint)
.setView(view)
.setNegativeButton(android.R.string.cancel, null)
.create();
}
@Override
public void onResume() {
super.onResume();
if (_helper != null) {
_helper.startListening(new FingerprintManager.CryptoObject(_cipher));
}
}
@Override
public void onPause() {
super.onPause();
if (_helper != null) {
_helper.stopListening();
}
}
@Override
public void onAuthenticated() {
getListener().onSlotResult(_slot, _cipher);
dismiss();
}
@Override
public void onError() {
}
}

View file

@ -1,80 +0,0 @@
package me.impy.aegis.ui.dialogs;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import javax.crypto.Cipher;
import me.impy.aegis.R;
import me.impy.aegis.db.slots.PasswordSlot;
import me.impy.aegis.db.slots.Slot;
import me.impy.aegis.db.slots.SlotException;
import me.impy.aegis.helpers.EditTextHelper;
import me.impy.aegis.ui.tasks.DerivationTask;
public class PasswordDialogFragment extends SlotDialogFragment {
private Button _buttonOK;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_password, null);
EditText textPassword = view.findViewById(R.id.text_password);
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
AlertDialog alert = new AlertDialog.Builder(getActivity())
.setTitle(R.string.set_password)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
alert.setOnShowListener(dialog -> {
_buttonOK = alert.getButton(AlertDialog.BUTTON_POSITIVE);
_buttonOK.setEnabled(false);
// replace the default listener
_buttonOK.setOnClickListener(v -> {
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm)) {
return;
}
char[] password = EditTextHelper.getEditTextChars(textPassword);
PasswordSlot slot = new PasswordSlot();
DerivationTask task = new DerivationTask(getActivity(), key -> {
Cipher cipher;
try {
cipher = Slot.createEncryptCipher(key);
} catch (SlotException e) {
getListener().onException(e);
dialog.cancel();
return;
}
getListener().onSlotResult(slot, cipher);
dialog.dismiss();
});
task.execute(new DerivationTask.Params(slot, password));
});
});
TextWatcher watcher = new TextWatcher() {
public void onTextChanged(CharSequence c, int start, int before, int count) {
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
_buttonOK.setEnabled(equal);
}
public void beforeTextChanged(CharSequence c, int start, int count, int after) { }
public void afterTextChanged(Editable c) { }
};
textPassword.addTextChangedListener(watcher);
textPasswordConfirm.addTextChangedListener(watcher);
return alert;
}
}

View file

@ -1,32 +0,0 @@
package me.impy.aegis.ui.dialogs;
import android.content.Context;
import androidx.fragment.app.DialogFragment;
import javax.crypto.Cipher;
import me.impy.aegis.db.slots.Slot;
public class SlotDialogFragment extends DialogFragment {
private Listener _listener;
protected Listener getListener() {
return _listener;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
_listener = (Listener) context;
} catch (ClassCastException e) {
throw new ClassCastException(context.toString() + " must implement SlotDialogFragment.Listener");
}
}
public interface Listener {
void onSlotResult(Slot slot, Cipher cipher);
void onException(Exception e);
}
}

View file

@ -25,6 +25,34 @@
android:layout_height="wrap_content"
android:scrollbars="vertical"/>
<LinearLayout
android:id="@+id/button_add_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="12.5dp"
android:paddingBottom="12.5dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_plus_black_24dp"
android:tint="@color/colorAccent"
android:layout_marginEnd="15dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_password"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
<LinearLayout
android:id="@+id/button_add_fingerprint"
android:layout_width="match_parent"

View file

@ -27,6 +27,8 @@
<string name="pref_secure_screen_summary">Block screenshots and other attempts to capture the screen within the app</string>
<string name="pref_encryption_title">Encryption</string>
<string name="pref_encryption_summary">Encrypt the database and unlock it with a password or fingerprint</string>
<string name="pref_fingerprint_title">Fingerprint</string>
<string name="pref_fingerprint_summary">Allow fingerprints registered on this device to unlock the vault</string>
<string name="pref_set_password_title">Change password</string>
<string name="pref_set_password_summary">Set a new password which you will need to unlock your vault</string>
@ -81,6 +83,7 @@
<string name="scan">Scan QR code</string>
<string name="enter_manually">Enter manually</string>
<string name="add_fingerprint">Add fingerprint</string>
<string name="add_password">Add password</string>
<string name="slots_warning">The vault is only as secure as your weakest secret. When a new fingerprint is added to your device, you will to reactivate fingerprint authentication within Aegis.</string>
<string name="copy">Copy</string>
<string name="edit">Edit</string>
@ -122,7 +125,8 @@
<string name="exporting_database_error">An error occurred while trying to export the database</string>
<string name="export_database_location">The database has been exported to:</string>
<string name="export_warning">This action will export the database 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_set_password_error">An error occurred while trying to set the password: </string>
<string name="encryption_enable_fingerprint_error">An error occurred while trying to enable fingerprint unlock: </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>

View file

@ -53,6 +53,13 @@
android:dependency="pref_encryption"
app:iconSpaceReserved="false"/>
<me.impy.aegis.ui.preferences.SwitchPreference
android:key="pref_fingerprint"
android:title="@string/pref_fingerprint_title"
android:summary="@string/pref_fingerprint_summary"
android:persistent="false"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_slots"
android:title="@string/pref_slots_title"