/* This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. Keepass2Android is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Keepass2Android is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Keepass2Android. If not, see . */ using System; using System.Linq; using Android.App; using Android.Content; using Android.OS; using Android.Views; using Android.Widget; using Android.Content.PM; using KeePassLib.Keys; using Android.Preferences; using Android.Provider; using Android.Runtime; using Android.Views.InputMethods; using Google.Android.Material.AppBar; using Google.Android.Material.Dialog; using keepass2android; using KeePassLib; using KeePassLib.Serialization; using Toolbar = AndroidX.AppCompat.Widget.Toolbar; using AndroidX.Core.Content; using keepass2android.Utils; namespace keepass2android { [Activity(Label = "@string/app_name", ConfigurationChanges = ConfigChanges.Orientation, WindowSoftInputMode = SoftInput.AdjustResize, MainLauncher = false, Theme = "@style/Kp2aTheme_BlueNoActionBar")] public class QuickUnlock : LifecycleAwareActivity, IBiometricAuthCallback { private IOConnectionInfo _ioc; private QuickUnlockBroadcastReceiver _intentReceiver; private ActivityDesign _design; private IBiometricIdentifier _biometryIdentifier; private int _quickUnlockLength; private int numFailedAttempts = 0; private int maxNumFailedAttempts = int.MaxValue; public QuickUnlock() { _design = new ActivityDesign(this); } protected override void OnCreate(Bundle bundle) { _design.ApplyTheme(); base.OnCreate(bundle); //use FlagSecure to make sure the last (revealed) character of the password is not visible in recent apps Util.MakeSecureDisplay(this); _ioc = App.Kp2a.GetDbForQuickUnlock()?.Ioc; if (_ioc == null) { Finish(); return; } SetContentView(Resource.Layout.QuickUnlock); var collapsingToolbar = FindViewById(Resource.Id.collapsing_toolbar); collapsingToolbar.SetTitle(GetString(Resource.String.QuickUnlock_prefs)); SetSupportActionBar(FindViewById(Resource.Id.toolbar)); if (App.Kp2a.GetDbForQuickUnlock().KpDatabase.Name != "") { FindViewById(Resource.Id.filename_label).Visibility = ViewStates.Visible; ((TextView) FindViewById(Resource.Id.filename_label)).Text = App.Kp2a.GetDbForQuickUnlock().KpDatabase.Name; } else { if ( PreferenceManager.GetDefaultSharedPreferences(this) .GetBoolean(GetString(Resource.String.RememberRecentFiles_key), Resources.GetBoolean(Resource.Boolean.RememberRecentFiles_default))) { ((TextView) FindViewById(Resource.Id.filename_label)).Text = App.Kp2a.GetFileStorage(_ioc).GetDisplayName(_ioc); } else { ((TextView) FindViewById(Resource.Id.filename_label)).Text = "*****"; } } TextView txtLabel = (TextView) FindViewById(Resource.Id.QuickUnlock_label); _quickUnlockLength = App.Kp2a.QuickUnlockKeyLength; bool useUnlockKeyFromDatabase = QuickUnlockFromDatabaseEnabled && FindQuickUnlockEntry() != null; if (useUnlockKeyFromDatabase || PreferenceManager.GetDefaultSharedPreferences(this) .GetBoolean(GetString(Resource.String.QuickUnlockHideLength_key), false)) { txtLabel.Text = GetString(Resource.String.QuickUnlock_label_secure); } else { txtLabel.Text = GetString(Resource.String.QuickUnlock_label, new Java.Lang.Object[] { _quickUnlockLength }); } EditText pwd = (EditText) FindViewById(Resource.Id.QuickUnlock_password); pwd.SetEms(_quickUnlockLength); Util.MoveBottomBarButtons(Resource.Id.QuickUnlock_buttonLock, Resource.Id.QuickUnlock_button, Resource.Id.bottom_bar, this); Button btnUnlock = (Button) FindViewById(Resource.Id.QuickUnlock_button); btnUnlock.Click += (object sender, EventArgs e) => { OnUnlock(pwd); }; Button btnLock = (Button) FindViewById(Resource.Id.QuickUnlock_buttonLock); btnLock.Text = btnLock.Text.Replace("ß", "ss"); btnLock.Click += (object sender, EventArgs e) => { App.Kp2a.Lock(false); Finish(); }; pwd.EditorAction += (sender, args) => { if ((args.ActionId == ImeAction.Done) || ((args.ActionId == ImeAction.ImeNull) && (args.Event.Action == KeyEventActions.Down))) OnUnlock(pwd); }; _intentReceiver = new QuickUnlockBroadcastReceiver(this); IntentFilter filter = new IntentFilter(); filter.AddAction(Intents.DatabaseLocked); ContextCompat.RegisterReceiver(this, _intentReceiver, filter, (int)ReceiverFlags.Exported); Util.SetNoPersonalizedLearning(FindViewById(Resource.Id.QuickUnlock_password)); if (bundle != null) numFailedAttempts = bundle.GetInt(NumFailedAttemptsKey, 0); FindViewById(Resource.Id.QuickUnlock_buttonEnableLock).Click += (object sender, EventArgs e) => { Intent intent = new Intent(Settings.ActionSecuritySettings); StartActivity(intent); }; FindViewById(Resource.Id.QuickUnlock_buttonCloseDb).Click += (object sender, EventArgs e) => { App.Kp2a.Lock(false); }; if (App.Kp2a.ScreenLockWasEnabledWhenOpeningDatabase == false) { FindViewById(Resource.Id.QuickUnlockForm).Visibility = ViewStates.Gone; FindViewById(Resource.Id.QuickUnlockBlocked).Visibility = ViewStates.Visible; } else { FindViewById(Resource.Id.QuickUnlockForm).Visibility = ViewStates.Visible; FindViewById(Resource.Id.QuickUnlockBlocked).Visibility = ViewStates.Gone; } } private bool QuickUnlockFromDatabaseEnabled => PreferenceManager.GetDefaultSharedPreferences(this) .GetBoolean(GetString(Resource.String.QuickUnlockKeyFromDatabase_key), false); private static PwEntry FindQuickUnlockEntry() { return App.Kp2a.GetDbForQuickUnlock()?.KpDatabase?.RootGroup?.Entries.SingleOrDefault(e => e.Strings.GetSafe(PwDefs.TitleField).ReadString() == "QuickUnlock"); } private const string NumFailedAttemptsKey = "FailedAttempts"; protected override void OnSaveInstanceState(Bundle outState) { base.OnSaveInstanceState(outState); outState.PutInt(NumFailedAttemptsKey, numFailedAttempts); } protected override void OnStart() { base.OnStart(); DonateReminder.ShowDonateReminderIfAppropriate(this); } public void OnBiometricError(string message) { Kp2aLog.Log("fingerprint error: " + message); var btn = FindViewById(Resource.Id.fingerprintbtn); btn.SetImageResource(Resource.Drawable.ic_fingerprint_error); btn.PostDelayed(() => { btn.SetImageResource(Resource.Drawable.baseline_fingerprint_24); }, 1300); App.Kp2a.ShowMessage(this, message, MessageSeverity.Error); } public void OnBiometricAttemptFailed(string message) { numFailedAttempts++; if (numFailedAttempts >= maxNumFailedAttempts) { FindViewById(Resource.Id.fingerprintbtn).Visibility = ViewStates.Gone; _biometryIdentifier.StopListening(); } } public void OnBiometricAuthSucceeded() { Kp2aLog.Log("OnFingerprintAuthSucceeded"); _biometryIdentifier.StopListening(); var btn = FindViewById(Resource.Id.fingerprintbtn); btn.SetImageResource(Resource.Drawable.ic_fingerprint_success); EditText pwd = (EditText)FindViewById(Resource.Id.QuickUnlock_password); pwd.Text = ExpectedPasswordPart; btn.PostDelayed(() => { UnlockAndSyncAndClose(); }, 500); } private bool InitFingerprintUnlock() { Kp2aLog.Log("InitFingerprintUnlock"); if (_biometryIdentifier != null) { Kp2aLog.Log("Already listening for fingerprint!"); return true; } var btn = FindViewById(Resource.Id.fingerprintbtn); try { FingerprintUnlockMode um; Enum.TryParse(PreferenceManager.GetDefaultSharedPreferences(this).GetString(App.Kp2a.GetDbForQuickUnlock().CurrentFingerprintModePrefKey, ""), out um); btn.Visibility = (um != FingerprintUnlockMode.Disabled) ? ViewStates.Visible : ViewStates.Gone; if (um == FingerprintUnlockMode.Disabled) { _biometryIdentifier = null; return false; } if (um == FingerprintUnlockMode.QuickUnlock && Util.GetCloseDatabaseAfterFailedBiometricQuickUnlock(this)) { maxNumFailedAttempts = 3; } BiometricModule fpModule = new BiometricModule(this); Kp2aLog.Log("fpModule.IsHardwareAvailable=" + fpModule.IsHardwareAvailable); if (fpModule.IsHardwareAvailable) //see FingerprintSetupActivity _biometryIdentifier = new BiometricDecryption(fpModule, App.Kp2a.GetDbForQuickUnlock().CurrentFingerprintPrefKey, this, App.Kp2a.GetDbForQuickUnlock().CurrentFingerprintPrefKey); if (_biometryIdentifier == null) { FindViewById(Resource.Id.fingerprintbtn).Visibility = ViewStates.Gone; return false; } if (_biometryIdentifier.Init()) { Kp2aLog.Log("successfully initialized fingerprint."); btn.SetImageResource(Resource.Drawable.baseline_fingerprint_24); _biometryIdentifier.StartListening(this); return true; } else { Kp2aLog.Log("failed to initialize fingerprint."); HandleFingerprintKeyInvalidated(); } } catch (Exception e) { Kp2aLog.Log("Error initializing Fingerprint Unlock: " + e); btn.SetImageResource(Resource.Drawable.ic_fingerprint_error); btn.Tag = "Error initializing Fingerprint Unlock: " + e; _biometryIdentifier = null; } return false; } private void HandleFingerprintKeyInvalidated() { var btn = FindViewById(Resource.Id.fingerprintbtn); //key invalidated permanently btn.SetImageResource(Resource.Drawable.ic_fingerprint_error); btn.Tag = GetString(Resource.String.fingerprint_unlock_failed) + " " + GetString(Resource.String.fingerprint_reenable2); _biometryIdentifier = null; } private void OnUnlock(EditText pwd) { var expectedPasswordPart = ExpectedPasswordPart; if (pwd.Text == expectedPasswordPart) { UnlockAndSyncAndClose(); } else { Kp2aLog.Log("QuickUnlock not successful!"); App.Kp2a.Lock(false); App.Kp2a.ShowMessage(this, GetString(Resource.String.QuickUnlock_fail), MessageSeverity.Error); Finish(); } } private void UnlockAndSyncAndClose() { App.Kp2a.UnlockDatabase(); if (PreferenceManager.GetDefaultSharedPreferences(this) .GetBoolean(GetString(Resource.String.SyncAfterQuickUnlock_key), false)) { new SyncUtil(this).SynchronizeDatabase(Finish); } else Finish(); } private string ExpectedPasswordPart { get { if (QuickUnlockFromDatabaseEnabled) { var quickUnlockEntry = FindQuickUnlockEntry(); if (quickUnlockEntry != null) { return quickUnlockEntry.Strings.ReadSafe(PwDefs.PasswordField).ToString(); } } KcpPassword kcpPassword = (KcpPassword) App.Kp2a.GetDbForQuickUnlock().KpDatabase.MasterKey.GetUserKey(typeof (KcpPassword)); String password = kcpPassword.Password.ReadString(); var passwordStringInfo = new System.Globalization.StringInfo(password); int passwordLength = passwordStringInfo.LengthInTextElements; String expectedPasswordPart = passwordStringInfo.SubstringByTextElements(Math.Max(0, passwordLength - _quickUnlockLength), Math.Min(passwordLength, _quickUnlockLength)); return expectedPasswordPart; } } private void OnLockDatabase() { CheckIfUnloaded(); } protected override void OnResume() { base.OnResume(); _design.ReapplyTheme(); App.Kp2a.MessagePresenter = new ChainedSnackbarPresenter(FindViewById(Resource.Id.main_content)); CheckIfUnloaded(); InitFingerprintUnlock(); bool showKeyboard = true; EditText pwd = (EditText)FindViewById(Resource.Id.QuickUnlock_password); pwd.PostDelayed(() => { pwd.RequestFocus(); InputMethodManager keyboard = (InputMethodManager)GetSystemService(Context.InputMethodService); if (showKeyboard) keyboard.ShowSoftInput(pwd, ShowFlags.Implicit); else keyboard.HideSoftInputFromWindow(pwd.WindowToken, HideSoftInputFlags.ImplicitOnly); }, 50); var btn = FindViewById(Resource.Id.fingerprintbtn); btn.Click += (sender, args) => { if ((_biometryIdentifier != null) && ((_biometryIdentifier.HasUserInterface)|| string.IsNullOrEmpty((string)btn.Tag) )) { _biometryIdentifier.StartListening(this); } else { MaterialAlertDialogBuilder b = new MaterialAlertDialogBuilder(this); b.SetTitle(Resource.String.fingerprint_prefs); b.SetMessage(btn.Tag.ToString()); b.SetPositiveButton(Android.Resource.String.Ok, (o, eventArgs) => ((Dialog)o).Dismiss()); if (_biometryIdentifier != null) { b.SetNegativeButton(Resource.String.disable_sensor, (senderAlert, alertArgs) => { btn.SetImageResource(Resource.Drawable.ic_fingerprint_error); _biometryIdentifier?.StopListening(); _biometryIdentifier = null; }); } else { b.SetNegativeButton(Resource.String.enable_sensor, (senderAlert, alertArgs) => { InitFingerprintUnlock(); }); } b.Show(); } }; } protected override void OnPause() { App.Kp2a.MessagePresenter = new NonePresenter(); if (_biometryIdentifier != null) { Kp2aLog.Log("FP: Stop listening"); _biometryIdentifier.StopListening(); } base.OnPause(); } protected override void OnDestroy() { base.OnDestroy(); try { UnregisterReceiver(_intentReceiver); } catch (Exception e) { Kp2aLog.LogUnexpectedError(e); } } private void CheckIfUnloaded() { if (App.Kp2a.OpenDatabases.Any() == false) { Finish(); } } public override void OnBackPressed() { SetResult(KeePass.ExitClose); base.OnBackPressed(); } private class QuickUnlockBroadcastReceiver : BroadcastReceiver { readonly QuickUnlock _activity; public QuickUnlockBroadcastReceiver(QuickUnlock activity) { _activity = activity; } public override void OnReceive(Context context, Intent intent) { switch (intent.Action) { case Intents.DatabaseLocked: _activity.OnLockDatabase(); break; } } } } }