diff --git a/src/keepass2android/FingerprintModule.cs b/src/keepass2android/FingerprintModule.cs new file mode 100644 index 00000000..6abac0c9 --- /dev/null +++ b/src/keepass2android/FingerprintModule.cs @@ -0,0 +1,434 @@ +using System; +using Android.Content; +using Javax.Crypto; +using Java.Security; +using Java.Lang; +using Android.Views.InputMethods; +using Android.App; +using Android.Hardware.Fingerprints; +using Android.OS; +using Android.Security.Keystore; +using Android.Preferences; +using Android.Util; +using Android.Widget; +using Java.IO; +using Java.Security.Cert; +using Javax.Crypto.Spec; + +namespace keepass2android +{ + public class FingerprintModule + { + public Context Context { get; set; } + + public FingerprintModule (Context context) + { + Context = context; + } + + public FingerprintManager FingerprintManager + { + get { return (FingerprintManager) Context.GetSystemService(Context.FingerprintService); } + } + + public KeyguardManager KeyguardManager + { + get + { + return (KeyguardManager) Context.GetSystemService("keyguard"); + } + } + + + public KeyStore Keystore + { + get + { + try + { + return KeyStore.GetInstance("AndroidKeyStore"); + } + catch (KeyStoreException e) + { + throw new RuntimeException("Failed to get an instance of KeyStore", e); + } + } + } + + public KeyGenerator KeyGenerator + { + get + { + try + { + return KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, "AndroidKeyStore"); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException("Failed to get an instance of KeyGenerator", e); + } + catch (NoSuchProviderException e) + { + throw new RuntimeException("Failed to get an instance of KeyGenerator", e); + } + } + } + + public Cipher Cipher + { + get + { + try + { + return Cipher.GetInstance(KeyProperties.KeyAlgorithmAes + "/" + + KeyProperties.BlockModeCbc + "/" + + KeyProperties.EncryptionPaddingPkcs7); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException("Failed to get an instance of Cipher", e); + } + catch (NoSuchPaddingException e) + { + throw new RuntimeException("Failed to get an instance of Cipher", e); + } + } + } + + public InputMethodManager InputMethodManager + { + get { return (InputMethodManager) Context.GetSystemService(Context.InputMethodService); } + } + + public ISharedPreferences SharedPreferences + { + get { return PreferenceManager.GetDefaultSharedPreferences(Context); } + } + } + + public abstract class FingerprintCrypt: FingerprintManager.AuthenticationCallback + { + protected const string FailedToInitCipher = "Failed to init Cipher"; + public override void OnAuthenticationError(FingerprintState errorCode, ICharSequence errString) + { + Kp2aLog.Log("FP: OnAuthenticationError: " + errString + ", " + _selfCancelled); + if (!_selfCancelled) + _callback.OnAuthenticationError(errorCode, errString); + } + + public override void OnAuthenticationFailed() + { + Kp2aLog.Log("FP: OnAuthenticationFailed " + _selfCancelled); + _callback.OnAuthenticationFailed(); + } + + public override void OnAuthenticationHelp(FingerprintState helpCode, ICharSequence helpString) + { + _callback.OnAuthenticationHelp(helpCode, helpString); + } + + public override void OnAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) + { + Kp2aLog.Log("FP: OnAuthenticationSucceeded "); + StopListening(); + _callback.OnAuthenticationSucceeded(result); + } + + protected readonly string _keyId; + + protected Cipher _cipher; + private bool _selfCancelled; + private CancellationSignal _cancellationSignal; + protected FingerprintManager.CryptoObject _cryptoObject; + private FingerprintManager.AuthenticationCallback _callback; + protected KeyStore _keystore; + + private FingerprintManager _fingerprintManager; + + public FingerprintCrypt(FingerprintModule fingerprint, string keyId) + { + Kp2aLog.Log("FP: Create " + this.GetType().Name); + _keyId = keyId; + + _cipher = fingerprint.Cipher; + _keystore = fingerprint.Keystore; + + _fingerprintManager = fingerprint.FingerprintManager; + + } + + public abstract bool InitCipher(); + protected static string GetAlias(string keyId) + { + return "keepass2android." + keyId; + } + protected static string GetIvPrefKey(string prefKey) + { + return prefKey + "_iv"; + } + public bool IsFingerprintAuthAvailable + { + get + { + return _fingerprintManager.IsHardwareDetected + && _fingerprintManager.HasEnrolledFingerprints; + } + } + + public void StartListening(FingerprintManager.AuthenticationCallback callback) + { + if (!IsFingerprintAuthAvailable) + return; + + Kp2aLog.Log("FP: StartListening "); + _cancellationSignal = new CancellationSignal(); + _selfCancelled = false; + _callback = callback; + _fingerprintManager.Authenticate(_cryptoObject, _cancellationSignal, 0 /* flags */, this, null); + + } + + public void StopListening() + { + if (_cancellationSignal != null) + { + Kp2aLog.Log("FP: StopListening "); + _selfCancelled = true; + _cancellationSignal.Cancel(); + _cancellationSignal = null; + } + } + + public string Encrypt(string textToEncrypt) + { + Kp2aLog.Log("FP: Encrypting"); + return Base64.EncodeToString(_cipher.DoFinal(System.Text.Encoding.UTF8.GetBytes(textToEncrypt)), 0); + } + + + public void StoreEncrypted(string textToEncrypt, string prefKey, Context context) + { + var edit = PreferenceManager.GetDefaultSharedPreferences(context).Edit(); + StoreEncrypted(textToEncrypt, prefKey, edit); + edit.Commit(); + } + + public void StoreEncrypted(string textToEncrypt, string prefKey, ISharedPreferencesEditor edit) + { + edit.PutString(prefKey, Encrypt(textToEncrypt)); + edit.PutString(GetIvPrefKey(prefKey), Base64.EncodeToString(CipherIv, 0)); + + } + + + private byte[] CipherIv + { + get { return _cipher.GetIV(); } + } + } + + public class FingerprintDecryption : FingerprintCrypt + { + private readonly Context _context; + private readonly byte[] _iv; + + + public FingerprintDecryption(FingerprintModule fingerprint, string keyId, byte[] iv) : base(fingerprint, keyId) + { + _iv = iv; + } + + public FingerprintDecryption(FingerprintModule fingerprint, string keyId, Context context, string prefKey) + : base(fingerprint, keyId) + { + _context = context; + _iv = Base64.Decode(PreferenceManager.GetDefaultSharedPreferences(context).GetString(GetIvPrefKey(prefKey), null), 0); + } + + public override bool InitCipher() + { + Kp2aLog.Log("FP: InitCipher for Dec"); + try + { + _keystore.Load(null); + var key = _keystore.GetKey(GetAlias(_keyId), null); + var ivParams = new IvParameterSpec(_iv); + _cipher.Init(CipherMode.DecryptMode, key, ivParams); + + _cryptoObject = new FingerprintManager.CryptoObject(_cipher); + return true; + } + catch (KeyPermanentlyInvalidatedException) + { + return false; + } + catch (KeyStoreException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (CertificateException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (UnrecoverableKeyException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (IOException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (InvalidKeyException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + } + + + public string Decrypt(string encryted) + { + Kp2aLog.Log("FP: Decrypting "); + byte[] encryptedBytes = Base64.Decode(encryted, 0); + return System.Text.Encoding.UTF8.GetString(_cipher.DoFinal(encryptedBytes)); + } + + public string DecryptStored(string prefKey) + { + string enc = PreferenceManager.GetDefaultSharedPreferences(_context).GetString(prefKey, null); + return Decrypt(enc); + } + } + + public class FingerprintEncryption : FingerprintCrypt + { + + private KeyGenerator _keyGen; + + + public FingerprintEncryption(FingerprintModule fingerprint, string keyId) : + base(fingerprint, keyId) + { + _keyGen = fingerprint.KeyGenerator; + Kp2aLog.Log("FP: CreateKey "); + CreateKey(); + } + + + /// + /// Creates a symmetric key in the Android Key Store which can only be used after the user + /// has authenticated with fingerprint. + /// + private void CreateKey() + { + try + { + _keystore.Load(null); + _keyGen.Init(new KeyGenParameterSpec.Builder(GetAlias(_keyId), + KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt) + .SetBlockModes(KeyProperties.BlockModeCbc) + // Require the user to authenticate with a fingerprint to authorize every use + // of the key + .SetUserAuthenticationRequired(true) + .SetEncryptionPaddings(KeyProperties.EncryptionPaddingPkcs7) + .Build()); + _keyGen.GenerateKey(); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(e); + } + catch (InvalidAlgorithmParameterException e) + { + throw new RuntimeException(e); + } + catch (CertificateException e) + { + throw new RuntimeException(e); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + public override bool InitCipher() + { + Kp2aLog.Log("FP: InitCipher for Enc "); + try + { + _keystore.Load(null); + var key = _keystore.GetKey(GetAlias(_keyId), null); + _cipher.Init(CipherMode.EncryptMode, key); + + _cryptoObject = new FingerprintManager.CryptoObject(_cipher); + return true; + } + catch (KeyPermanentlyInvalidatedException) + { + return false; + } + catch (KeyStoreException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (CertificateException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (UnrecoverableKeyException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (IOException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (InvalidKeyException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + } + } + + public class FingerprintAuthCallbackAdapter : FingerprintManager.AuthenticationCallback + { + private readonly IFingerprintAuthCallback _callback; + private readonly Context _context; + + public FingerprintAuthCallbackAdapter(IFingerprintAuthCallback callback, Context context) + { + _callback = callback; + _context = context; + } + + public override void OnAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) + { + _callback.OnFingerprintAuthSucceeded(); + } + + public override void OnAuthenticationError(FingerprintState errorCode, ICharSequence errString) + { + _callback.OnFingerprintError(errString.ToString()); + } + + public override void OnAuthenticationHelp(FingerprintState helpCode, ICharSequence helpString) + { + _callback.OnFingerprintError(helpString.ToString()); + } + + public override void OnAuthenticationFailed() + { + _callback.OnFingerprintError( + _context.Resources.GetString(Resource.String.fingerprint_not_recognized)); + } + } + +} \ No newline at end of file diff --git a/src/keepass2android/FingerprintSetupActivity.cs b/src/keepass2android/FingerprintSetupActivity.cs new file mode 100644 index 00000000..aaef9ed7 --- /dev/null +++ b/src/keepass2android/FingerprintSetupActivity.cs @@ -0,0 +1,313 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Android; +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.Hardware.Fingerprints; +using Android.OS; +using Android.Preferences; +using Android.Runtime; +using Android.Views; +using Android.Widget; +using Java.Lang; +using KeePassLib.Keys; +using KeePassLib.Utility; +using Enum = System.Enum; +using Exception = System.Exception; + +namespace keepass2android +{ + [Activity(Label = "@string/app_name", + ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden, + Theme = "@style/MyTheme_ActionBar", MainLauncher = false)] + [IntentFilter(new[] { "kp2a.action.FingerprintSetupActivity" }, Categories = new[] { Intent.CategoryDefault })] + public class FingerprintSetupActivity : LockCloseActivity + { + private readonly ActivityDesign _activityDesign; + + public FingerprintSetupActivity(IntPtr javaReference, JniHandleOwnership transfer) + : base(javaReference, transfer) + { + _activityDesign = new ActivityDesign(this); + } + public FingerprintSetupActivity() + { + _activityDesign = new ActivityDesign(this); + } + + + + private FingerprintUnlockMode _unlockMode = FingerprintUnlockMode.Disabled; + private FingerprintUnlockMode _desiredUnlockMode; + private FingerprintEncryption _enc; + private RadioButton[] _radioButtons; + public override bool OnOptionsItemSelected(IMenuItem item) + { + switch (item.ItemId) + { + + case Android.Resource.Id.Home: + Finish(); + return true; + } + + return base.OnOptionsItemSelected(item); + } + + protected override void OnCreate(Bundle savedInstanceState) + { + _activityDesign.ApplyTheme(); + base.OnCreate(savedInstanceState); + SetContentView(Resource.Layout.fingerprint_setup); + + Enum.TryParse( + PreferenceManager.GetDefaultSharedPreferences(this).GetString(App.Kp2a.GetDb().CurrentFingerprintModePrefKey, ""), + out _unlockMode); + + _fpIcon = FindViewById(Resource.Id.fingerprint_icon); + _fpTextView = FindViewById(Resource.Id.fingerprint_status); + + SupportActionBar.SetDisplayHomeAsUpEnabled(true); + SupportActionBar.SetHomeButtonEnabled(true); + + int[] radioButtonIds = + { + Resource.Id.radio_fingerprint_quickunlock, Resource.Id.radio_fingerprint_unlock, + Resource.Id.radio_fingerprint_disabled + }; + _radioButtons = radioButtonIds.Select(FindViewById).ToArray(); + _radioButtons[0].Tag = FingerprintUnlockMode.QuickUnlock.ToString(); + _radioButtons[1].Tag = FingerprintUnlockMode.FullUnlock.ToString(); + _radioButtons[2].Tag = FingerprintUnlockMode.Disabled.ToString(); + foreach (RadioButton r in _radioButtons) + { + r.CheckedChange += (sender, args) => + { + var rbSender = ((RadioButton) sender); + if (!rbSender.Checked) return; + foreach (RadioButton rOther in _radioButtons) + { + if (rOther == sender) continue; + rOther.Checked = false; + } + FingerprintUnlockMode newMode; + Enum.TryParse(rbSender.Tag.ToString(), out newMode); + ChangeUnlockMode(_unlockMode, newMode); + + }; + } + + CheckCurrentRadioButton(); + + int errorId = Resource.String.fingerprint_os_error; + SetError(errorId); + + FindViewById(Resource.Id.cancel_button).Click += (sender, args) => + { + _enc.StopListening(); + _unlockMode = FingerprintUnlockMode.Disabled; //cancelling a FingerprintEncryption means a new key has been created but not been authenticated to encrypt something. We can't keep the previous state. + StoreUnlockMode(); + FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Visible; + FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone; + _enc = null; + CheckCurrentRadioButton(); + }; + + FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone; + + if ((int)Build.VERSION.SdkInt >= 23) + RequestPermissions(new[] { Manifest.Permission.UseFingerprint }, FingerprintPermissionRequestCode); + } + + string CurrentPreferenceKey + { + get { return App.Kp2a.GetDb().CurrentFingerprintPrefKey; } + } + + private void StoreUnlockMode() + { + ISharedPreferencesEditor edit = PreferenceManager.GetDefaultSharedPreferences(this).Edit(); + if (_unlockMode == FingerprintUnlockMode.Disabled) + { + edit.PutString(CurrentPreferenceKey, ""); + } + else + { + if (_unlockMode == FingerprintUnlockMode.FullUnlock) + _enc.StoreEncrypted(App.Kp2a.GetDb().KpDatabase.MasterKey.GetUserKey().Password.ReadString(), CurrentPreferenceKey, edit); + else + _enc.StoreEncrypted("QuickUnlock" /*some dummy data*/, CurrentPreferenceKey, edit); + } + edit.PutString(App.Kp2a.GetDb().CurrentFingerprintModePrefKey, _unlockMode.ToString()); + edit.Commit(); + } + + private void CheckCurrentRadioButton() + { + + foreach (RadioButton r in _radioButtons) + { + FingerprintUnlockMode um; + Enum.TryParse(r.Tag.ToString(), out um); + if (um == _unlockMode) + r.Checked = true; + } + } + + private void SetError(int errorId) + { + var tv = FindViewById(Resource.Id.tvFatalError); + tv.Text = GetString(Resource.String.fingerprint_fatal) + " " + GetString(errorId); + tv.Visibility = ViewStates.Visible; + } + + const int FingerprintPermissionRequestCode = 0; + public override void OnRequestPermissionsResult (int requestCode, string[] permissions, Permission[] grantResults) + { + if (requestCode == FingerprintPermissionRequestCode && grantResults[0] == Permission.Granted) + { + FingerprintModule fpModule = new FingerprintModule(this); + if (!fpModule.FingerprintManager.IsHardwareDetected) + { + SetError(Resource.String.fingerprint_hardware_error); + return; + } + if (!fpModule.FingerprintManager.HasEnrolledFingerprints) + { + SetError(Resource.String.fingerprint_no_enrolled); + return; + } + FindViewById(Resource.Id.tvFatalError).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Visible; + FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone; + } + } + + + private void ChangeUnlockMode(FingerprintUnlockMode oldMode, FingerprintUnlockMode newMode) + { + if (oldMode == newMode) + return; + + if (newMode == FingerprintUnlockMode.Disabled) + { + _unlockMode = newMode; + StoreUnlockMode(); + return; + } + + _desiredUnlockMode = newMode; + FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Visible; + + _enc = new FingerprintEncryption(new FingerprintModule(this), CurrentPreferenceKey); + try + { + if (!_enc.InitCipher()) + throw new Exception("Failed to initialize cipher"); + ResetErrorTextRunnable(); + _enc.StartListening(new SetupCallback(this)); + } + catch (Exception e) + { + CheckCurrentRadioButton(); + Toast.MakeText(this, e.ToString(), ToastLength.Long).Show(); + FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Visible; + FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone; + } + + + } + + static readonly long ERROR_TIMEOUT_MILLIS = 1600; + static readonly long SUCCESS_DELAY_MILLIS = 1300; + private ImageView _fpIcon; + private TextView _fpTextView; + public void OnAuthSucceeded() + { + _unlockMode = _desiredUnlockMode; + + _fpTextView.RemoveCallbacks(ResetErrorTextRunnable); + _fpIcon.SetImageResource(Resource.Drawable.ic_fingerprint_success); + _fpTextView.SetTextColor(_fpTextView.Resources.GetColor(Resource.Color.success_color, null)); + _fpTextView.Text = _fpTextView.Resources.GetString(Resource.String.fingerprint_success); + _fpIcon.PostDelayed(() => + { + FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Visible; + FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone; + + StoreUnlockMode(); + + }, SUCCESS_DELAY_MILLIS); + + + } + + + public void OnFingerprintError(string error) + { + _fpIcon.SetImageResource(Resource.Drawable.ic_fingerprint_error); + _fpTextView.Text = error; + _fpTextView.SetTextColor( + _fpTextView.Resources.GetColor(Resource.Color.warning_color, null)); + _fpTextView.RemoveCallbacks(ResetErrorTextRunnable); + _fpTextView.PostDelayed(ResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS); + } + + void ResetErrorTextRunnable() + { + _fpTextView.SetTextColor( + _fpTextView.Resources.GetColor(Resource.Color.hint_color, null)); + _fpTextView.Text = _fpTextView.Resources.GetString(Resource.String.fingerprint_hint); + _fpIcon.SetImageResource(Resource.Drawable.ic_fp_40px); + } + + protected override void OnResume() + { + base.OnResume(); + if (_enc != null) + _enc.StartListening(new SetupCallback(this)); + } + + protected override void OnPause() + { + base.OnPause(); + if (_enc != null) + _enc.StopListening(); + } + } + + internal class SetupCallback : FingerprintManager.AuthenticationCallback + { + private readonly FingerprintSetupActivity _fingerprintSetupActivity; + + public SetupCallback(FingerprintSetupActivity fingerprintSetupActivity) + { + _fingerprintSetupActivity = fingerprintSetupActivity; + } + + public override void OnAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) + { + _fingerprintSetupActivity.OnAuthSucceeded(); + } + + public override void OnAuthenticationError(FingerprintState errorCode, ICharSequence errString) + { + _fingerprintSetupActivity.OnFingerprintError(errString.ToString()); + } + + public override void OnAuthenticationHelp(FingerprintState helpCode, ICharSequence helpString) + { + _fingerprintSetupActivity.OnFingerprintError(helpString.ToString()); + } + + public override void OnAuthenticationFailed() + { + _fingerprintSetupActivity.OnFingerprintError(_fingerprintSetupActivity.Resources.GetString(Resource.String.fingerprint_not_recognized)); + } + } +} \ No newline at end of file diff --git a/src/keepass2android/FingerprintUnlockMode.cs b/src/keepass2android/FingerprintUnlockMode.cs new file mode 100644 index 00000000..bd80f262 --- /dev/null +++ b/src/keepass2android/FingerprintUnlockMode.cs @@ -0,0 +1,9 @@ +namespace keepass2android +{ + public enum FingerprintUnlockMode + { + Disabled = 0, + QuickUnlock = 1, + FullUnlock = 2, + } +} \ No newline at end of file diff --git a/src/keepass2android/QuickUnlock.cs b/src/keepass2android/QuickUnlock.cs index ce11d5b6..088c46ba 100644 --- a/src/keepass2android/QuickUnlock.cs +++ b/src/keepass2android/QuickUnlock.cs @@ -305,6 +305,15 @@ namespace keepass2android } } + protected override void OnPause() + { + base.OnPause(); + if (_fingerprintDec != null) + { + _fingerprintDec.StopListening(); + } + } + protected override void OnDestroy() { base.OnDestroy(); diff --git a/src/keepass2android/Resources/drawable-mdpi/ic_fp_40px.png b/src/keepass2android/Resources/drawable-mdpi/ic_fp_40px.png new file mode 100644 index 00000000..122f4425 Binary files /dev/null and b/src/keepass2android/Resources/drawable-mdpi/ic_fp_40px.png differ diff --git a/src/keepass2android/Resources/drawable-xhdpi/ic_fp_40px.png b/src/keepass2android/Resources/drawable-xhdpi/ic_fp_40px.png new file mode 100644 index 00000000..e1c9590b Binary files /dev/null and b/src/keepass2android/Resources/drawable-xhdpi/ic_fp_40px.png differ diff --git a/src/keepass2android/Resources/drawable/ic_fingerprint_error.xml b/src/keepass2android/Resources/drawable/ic_fingerprint_error.xml new file mode 100644 index 00000000..be46116d --- /dev/null +++ b/src/keepass2android/Resources/drawable/ic_fingerprint_error.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/src/keepass2android/Resources/drawable/ic_fingerprint_success.xml b/src/keepass2android/Resources/drawable/ic_fingerprint_success.xml new file mode 100644 index 00000000..261f3e7f --- /dev/null +++ b/src/keepass2android/Resources/drawable/ic_fingerprint_success.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/src/keepass2android/Resources/layout/fingerprint_setup.xml b/src/keepass2android/Resources/layout/fingerprint_setup.xml new file mode 100644 index 00000000..40b8441c --- /dev/null +++ b/src/keepass2android/Resources/layout/fingerprint_setup.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + +