/* This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file is based on Keepassdroid, Copyright Brian Pellin. 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.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; using keepass2android; using Android.App; using Android.Content; using Android.Database; using Android.Graphics.Drawables; using Android.OS; using Android.Runtime; using Android.Views; using Android.Views.InputMethods; using Android.Widget; using Java.Net; using Android.Preferences; using Android.Text; using Android.Content.PM; using Android.Graphics; using AndroidX.AppCompat.App; using AndroidX.CoordinatorLayout.Widget; using AndroidX.Core.View; using AndroidX.DrawerLayout.Widget; using Google.Android.Material.AppBar; using Google.Android.Material.Dialog; using Java.Lang; using KeePassLib.Keys; using KeePassLib.Serialization; using Keepass2android.Pluginsdk; using OtpKeyProv; using keepass2android.Io; using keepass2android.Utils; using File = Java.IO.File; using FileNotFoundException = Java.IO.FileNotFoundException; using Object = Java.Lang.Object; using Process = Android.OS.Process; using KeeChallenge; using AlertDialog = Android.App.AlertDialog; using ClipboardManager = Android.Content.ClipboardManager; using Enum = System.Enum; using Exception = System.Exception; using String = System.String; using Toolbar = AndroidX.AppCompat.Widget.Toolbar; using AndroidX.Core.Content; namespace keepass2android { [Activity(Label = "@string/app_name", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden, LaunchMode = LaunchMode.SingleInstance, WindowSoftInputMode = SoftInput.AdjustResize, Theme = "@style/Kp2aTheme_BlueNoActionBar")] public class PasswordActivity : LockingActivity, IBiometricAuthCallback { enum KeyProviders { KeyFile = 1, Otp = 2, OtpRecovery = 3, Challenge = 4, ChalRecovery = 5, ChallengeXC = 6, //KeepassXC compatible Challenge-Response } public const String KeyDefaultFilename = "defaultFileName"; public const String KeyKeyfile = "keyFile"; public const String KeyPassword = "password"; public const String LaunchImmediately = "launchImmediately"; private const string ShowpasswordKey = "ShowPassword"; private const string KeyProviderIdOtp = "KP2A-OTP"; private const string KeyProviderIdOtpRecovery = "KP2A-OTPSecret"; private const string KeyProviderIdChallenge = "KP2A-Chal"; private const string KeyProviderIdChallengeRecovery = "KP2A-ChalSecret"; private const string KeyProviderIdChallengeXC = "KP2A-ChalXC"; private const int RequestCodePrepareDbFile = 1000; private const int RequestCodePrepareOtpAuxFile = 1001; private const int RequestCodeSelectKeyfile = 1003; private const int RequestCodePrepareKeyFile = 1004; private const int RequestCodeSelectAuxFile = 1005; public const int ResultSelectOtherFile = (int) Result.FirstUser; private Task _loadDbFileTask; private bool _loadDbTaskOffline; //indicate if preloading was started with offline mode private IOConnectionInfo _ioConnection; private String _keyFile; bool _showPassword; private bool _killOnDestroy; private string _password = ""; //OTPs which should be entered into the OTP fields as soon as these become visible private List _pendingOtps = new List(); private HashSet KeyProviderTypes = new HashSet(); private bool _rememberKeyfile; ISharedPreferences _prefs; private bool _starting; private bool _resumeCompleted; private OtpInfo _otpInfo; private IOConnectionInfo _otpAuxIoc; private ChallengeInfo _chalInfo; private byte[] _challengeSecret; private KeeChallengeProv _challengeProv; private readonly int[] _otpTextViewIds = new[] {Resource.Id.otp1, Resource.Id.otp2, Resource.Id.otp3, Resource.Id.otp4, Resource.Id.otp5, Resource.Id.otp6}; private const string OtpInfoKey = "OtpInfoKey"; private const string EnteredOtpsKey = "EnteredOtpsKey"; private const string PendingOtpsKey = "PendingOtpsKey"; private const string PasswordKey = "PasswordKey"; private const string KeyFileOrProviderKey = "KeyFileOrProviderKey"; private bool _performingLoad; private bool _keepPasswordInOnResume; private DateTime _lastOnPauseTime = DateTime.MinValue; private ActionBarDrawerToggle mDrawerToggle; private DrawerLayout _drawerLayout; private string mDrawerTitle; private MeasuringLinearLayout.MeasureArgs _measureArgs; private ActivityDesign _activityDesign; private BiometricDecryption _biometricDec; private PasswordActivityBroadcastReceiver _intentReceiver; private int _appnameclickCount; public int InvalidCompositeKeyCount { get; set; } public int LoadingErrorCount { get; set; } private bool fingerprintInitialized; public bool UsedFingerprintUnlock { get; set; } readonly PasswordFont _passwordFont = new PasswordFont(); private const string Kp2aKeyProviderStringPrefix = "_KP2A_KEYTYPES:"; //can be set before launching the Activity. Will be used once to immediately open the database static CompositeKey compositeKeyForImmediateLoad = null; private bool _makeCurrent; public PasswordActivity (IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { _activityDesign = new ActivityDesign(this); } public PasswordActivity() { _activityDesign = new ActivityDesign(this); } public static void Launch(Activity act, IOConnectionInfo ioc, CompositeKey compositeKey, ActivityLaunchMode launchMode, bool makeCurrent) { compositeKeyForImmediateLoad = compositeKey; Launch(act, ioc, launchMode, makeCurrent); } public static void Launch(Activity act, IOConnectionInfo ioc, ActivityLaunchMode launchMode, bool makeCurrent) { Intent i = new Intent(act, typeof(PasswordActivity)); Util.PutIoConnectionToIntent(ioc, i); i.PutExtra("MakeCurrent", makeCurrent); launchMode.Launch(act, i); } public void LaunchNextActivity() { //StackBaseActivity will launch the next activity Intent data = new Intent(); data.PutExtra("ioc", IOConnectionInfo.SerializeToString(_ioConnection)); SetResult(Result.Ok, data); Finish(); } protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) { base.OnActivityResult(requestCode, resultCode, data); _keepPasswordInOnResume = true; Kp2aLog.Log("PasswordActivity.OnActivityResult "+resultCode+"/"+requestCode); switch(resultCode) { case Result.Ok: if (requestCode == RequestCodeSelectKeyfile) { IOConnectionInfo ioc = new IOConnectionInfo(); Util.SetIoConnectionFromIntent(ioc, data); Kp2aLog.Log("Set keyfile after returning from RequestCodeSelectKeyfile"); _keyFile = IOConnectionInfo.SerializeToString(ioc); UpdateKeyfileIocView(); } break; case (Result)FileStorageResults.FileUsagePrepared: if (requestCode == RequestCodePrepareDbFile) { if (KeyProviderTypes.Contains(KeyProviders.KeyFile)) { //if the user has not yet selected a keyfile, _keyFile is empty if (string.IsNullOrEmpty(_keyFile) == false) { var iocKeyfile = IOConnectionInfo.UnserializeFromString(_keyFile); App.Kp2a.GetFileStorage(iocKeyfile) .PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), iocKeyfile, RequestCodePrepareKeyFile, false); } } else PerformLoadDatabase(); } if (requestCode == RequestCodePrepareKeyFile) { PerformLoadDatabase(); } if (requestCode == RequestCodePrepareOtpAuxFile) { GetAuxFileLoader().LoadAuxFile(true); } break; } if (requestCode == RequestCodeSelectAuxFile && resultCode == Result.Ok) { IOConnectionInfo auxFileIoc = new IOConnectionInfo(); Util.SetIoConnectionFromIntent(auxFileIoc, data); PreferenceManager.GetDefaultSharedPreferences(this).Edit() .PutString("KP2A.PasswordAct.AuxFileIoc" + IOConnectionInfo.SerializeToString(_ioConnection), IOConnectionInfo.SerializeToString(auxFileIoc)) .Apply(); GetAuxFileLoader().LoadAuxFile(false); } if (requestCode == RequestCodeChallengeYubikey) { if (CurrentlyWaitingKey != null) { //ActivityResult was handled in base class already return; } if (resultCode == Result.Ok) { try { byte[] challengeResponse = data.GetByteArrayExtra("response"); _challengeProv = new KeeChallengeProv(); _challengeSecret = _challengeProv.GetSecret(_chalInfo, challengeResponse); Array.Clear(challengeResponse, 0, challengeResponse.Length); } catch (Exception e) { Kp2aLog.Log(e.ToString()); App.Kp2a.ShowMessage(this, "Error: " + Util.GetErrorMessage(e), MessageSeverity.Error); return; } UpdateOkButtonState(); FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone; if (_challengeSecret != null) { new LoadingDialog(this, true, //doInBackground delegate { //save aux file try { ChallengeInfo temp = _challengeProv.Encrypt(_challengeSecret); if (!temp.Save(_otpAuxIoc)) { App.Kp2a.ShowMessage(this, Resource.String.ErrorUpdatingChalAuxFile, MessageSeverity.Error); return false; } } catch (Exception e) { Kp2aLog.LogUnexpectedError(e); } return null; } , delegate { }).Execute(); } else { App.Kp2a.ShowMessage(this, Resource.String.bad_resp, MessageSeverity.Error); } } } } private AuxFileLoader GetAuxFileLoader() { if (KeyProviderTypes.Contains(KeyProviders.Challenge)) { return new ChallengeAuxFileLoader(this); } else { return new OtpAuxFileLoader(this); } } private void UpdateKeyfileIocView() { //store keyfile in the view so that we can show the selected keyfile again if the user switches to another key provider and back to key file FindViewById(Resource.Id.label_keyfilename).Tag = _keyFile; if (string.IsNullOrEmpty(_keyFile)) { FindViewById(Resource.Id.filestorage_label).Visibility = ViewStates.Gone; FindViewById(Resource.Id.filestorage_logo).Visibility = ViewStates.Gone; FindViewById(Resource.Id.label_keyfilename).Text = Resources.GetString(Resource.String.no_keyfile_selected); return; } var ioc = IOConnectionInfo.UnserializeFromString(_keyFile); string displayPath = App.Kp2a.GetFileStorage(ioc).GetDisplayName(ioc); int protocolSeparatorPos = displayPath.IndexOf("://", StringComparison.Ordinal); string protocolId = protocolSeparatorPos < 0 ? "file" : displayPath.Substring(0, protocolSeparatorPos); Drawable drawable = App.Kp2a.GetStorageIcon(protocolId); FindViewById(Resource.Id.filestorage_logo).SetImageDrawable(drawable); FindViewById(Resource.Id.filestorage_logo).Visibility = ViewStates.Visible; String title = App.Kp2a.GetStorageDisplayName(protocolId); FindViewById(Resource.Id.filestorage_label).Text = title; FindViewById(Resource.Id.filestorage_label).Visibility = ViewStates.Visible; FindViewById(Resource.Id.label_keyfilename).Text = protocolSeparatorPos < 0 ? displayPath : displayPath.Substring(protocolSeparatorPos + 3); } private abstract class AuxFileLoader { protected readonly PasswordActivity Activity; protected AuxFileLoader(PasswordActivity activity) { Activity = activity; } public void LoadAuxFile(bool triggerSelectAuxManuallyOnFailure) { new LoadingDialog(Activity, true, //doInBackground delegate { try { var iocAux = GetDefaultAuxLocation(); LoadFile(iocAux); if (Activity._chalInfo == null) { throw new Java.Lang.Exception("Failed to load challenge aux file"); } } catch (Exception e) { //this can happen e.g. if the file storage does not support GetParentPath Kp2aLog.Log(e.ToString()); //retry with saved ioc try { var savedManualIoc = IOConnectionInfo.UnserializeFromString( PreferenceManager.GetDefaultSharedPreferences(Activity).GetString( "KP2A.PasswordAct.AuxFileIoc" + IOConnectionInfo.SerializeToString(Activity._ioConnection), null)); LoadFile((savedManualIoc)); } catch (Exception e2) { Kp2aLog.LogUnexpectedError(e2); } } return null; } , delegate { if (!AuxDataLoaded) { if (triggerSelectAuxManuallyOnFailure) { Intent intent = new Intent(Activity, typeof(SelectStorageLocationActivity)); intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppGet, true); intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, false); intent.PutExtra(FileStorageSetupDefs.ExtraIsForSave, false); intent.PutExtra(SelectStorageLocationActivity.ExtraKeyWritableRequirements, (int)SelectStorageLocationActivityBase.WritableRequirements.WriteDemanded); Activity.StartActivityForResult(intent, RequestCodeSelectAuxFile); } else { App.Kp2a.ShowMessage(Activity,GetErrorMessage(), MessageSeverity.Error); } return; } HandleSuccess(); }).Execute(); } protected abstract bool AuxDataLoaded { get; } protected abstract void LoadFile(IOConnectionInfo iocAux); protected abstract void HandleSuccess(); protected abstract string GetErrorMessage(); protected abstract IOConnectionInfo GetDefaultAuxLocation(); } private class OtpAuxFileLoader : AuxFileLoader { public OtpAuxFileLoader(PasswordActivity activity) : base(activity) { } protected override bool AuxDataLoaded { get { return Activity._otpInfo != null; } } protected override void LoadFile(IOConnectionInfo iocAux) { Activity._otpInfo = OtpInfo.Load(iocAux); Activity._otpAuxIoc = iocAux; } private static IOConnectionInfo GetAuxFileIoc(IOConnectionInfo databaseIoc) { IFileStorage fileStorage = App.Kp2a.GetOtpAuxFileStorage(databaseIoc); var parentPath = fileStorage.GetParentPath(databaseIoc); var filename = fileStorage.GetFilenameWithoutPathAndExt(databaseIoc) + OathHotpKeyProv.AuxFileExt; IOConnectionInfo iocAux = fileStorage.GetFilePath(parentPath, filename); return iocAux; } protected override void HandleSuccess() { IList prefilledOtps = Activity._pendingOtps; Activity.ShowOtpEntry(prefilledOtps); Activity._pendingOtps.Clear(); } protected override string GetErrorMessage() { return Activity.GetString(Resource.String.CouldntLoadOtpAuxFile) + " " + Activity.GetString(Resource.String.CouldntLoadOtpAuxFile_Hint); } protected override IOConnectionInfo GetDefaultAuxLocation() { return GetAuxFileIoc(Activity._ioConnection); } } private class ChallengeAuxFileLoader : AuxFileLoader { public ChallengeAuxFileLoader(PasswordActivity activity) : base(activity) { } protected override void HandleSuccess() { var chalIntent = Activity.TryGetYubichallengeIntentOrPrompt(Activity._chalInfo.Challenge, true); if (chalIntent != null) { Activity.StartActivityForResult(chalIntent, RequestCodeChallengeYubikey); } } protected override string GetErrorMessage() { return Activity.GetString(Resource.String.CouldntLoadChalAuxFile) + " " + Activity.GetString( Resource.String.CouldntLoadChalAuxFile_Hint); } protected override bool AuxDataLoaded { get { return Activity._chalInfo != null; } } protected override void LoadFile(IOConnectionInfo iocAux) { Activity._chalInfo = ChallengeInfo.Load(iocAux); Activity._otpAuxIoc = iocAux; } protected override IOConnectionInfo GetDefaultAuxLocation() { IFileStorage fileStorage = App.Kp2a.GetOtpAuxFileStorage(Activity._ioConnection); IOConnectionInfo iocAux = fileStorage.GetFilePath( fileStorage.GetParentPath(Activity._ioConnection), fileStorage.GetFilenameWithoutPathAndExt(Activity._ioConnection) + ".xml"); return iocAux; } } private void ShowOtpEntry(IList prefilledOtps) { FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone; FindViewById(Resource.Id.otpEntry).Visibility = ViewStates.Visible; int c = 0; foreach (int otpId in _otpTextViewIds) { c++; var otpTextView = FindViewById(otpId); if (c <= prefilledOtps.Count) { otpTextView.Text = prefilledOtps[c - 1]; } else { otpTextView.Text = ""; } otpTextView.Hint = GetString(Resource.String.otp_hint, new Object[] {c}); otpTextView.SetFilters(new IInputFilter[] {new InputFilterLengthFilter((int) _otpInfo.OtpLength)}); if (c > _otpInfo.OtpsRequired) { otpTextView.Visibility = ViewStates.Gone; } else { otpTextView.TextChanged += (sender, args) => { UpdateOkButtonState(); }; } } } private void UncollapseToolbar() { AppBarLayout appbarLayout = FindViewById(Resource.Id.appbar); var tmp = appbarLayout.LayoutParameters; CoordinatorLayout.LayoutParams p = tmp.JavaCast(); var tmp2 = p.Behavior; var behavior = tmp2.JavaCast(); if (behavior == null) { p.Behavior = behavior = new AppBarLayout.Behavior(); } behavior.OnNestedFling(FindViewById(Resource.Id.main_content), appbarLayout, null, 0, -10000, false); } private void CollapseToolbar() { AppBarLayout appbarLayout = FindViewById(Resource.Id.appbar); ViewGroup.LayoutParams tmp = appbarLayout.LayoutParameters; CoordinatorLayout.LayoutParams p = tmp.JavaCast(); var tmp2 = p.Behavior; var behavior = tmp2.JavaCast(); if (behavior == null) { p.Behavior = behavior = new AppBarLayout.Behavior(); } behavior.OnNestedFling(FindViewById(Resource.Id.main_content), appbarLayout, null, 0, 200, true); } protected override void OnCreate(Bundle savedInstanceState) { _activityDesign.ApplyTheme(); base.OnCreate(savedInstanceState); _intentReceiver = new PasswordActivityBroadcastReceiver(this); IntentFilter filter = new IntentFilter(); filter.AddAction(Intent.ActionScreenOff); ContextCompat.RegisterReceiver(this, _intentReceiver, filter, (int)ReceiverFlags.Exported); //use FlagSecure to make sure the last (revealed) character of the master password is not visible in recent apps Util.MakeSecureDisplay(this); _prefs = PreferenceManager.GetDefaultSharedPreferences(this); _rememberKeyfile = _prefs.GetBoolean(GetString(Resource.String.keyfile_key), Resources.GetBoolean(Resource.Boolean.keyfile_default)); SetContentView(Resource.Layout.password); InitializeToolbar(); var passwordEdit = FindViewById(Resource.Id.password_edit); passwordEdit.TextChanged += (sender, args) => { _password = passwordEdit.Text; UpdateOkButtonState(); }; passwordEdit.EditorAction += (sender, args) => { if ((args.ActionId == ImeAction.Done) || ((args.ActionId == ImeAction.ImeNull) && (args.Event.Action == KeyEventActions.Down))) OnOk(); }; InitializeBottomBarButtons(); InitializeNavDrawerButtons(); InitializeTogglePasswordButton(); InitializeKeyfileBrowseButton(); InitializeOptionCheckboxes(); FindViewById(Resource.Id.pass_otpsecret).TextChanged += (sender, args) => UpdateOkButtonState(); InitializeToolbarCollapsing(); InitializeOtpSecretSpinner(); //Intent-specific //fill _ioConnection, _keyFile, _password, _keepPasswordInOnResume Intent i = Intent; _ioConnection = new IOConnectionInfo(); String action = i.Action; if ((action != null) && (action.Equals(Intents.StartWithOtp))) { Kp2aLog.Log("Launching with OTP"); if (!GetIocFromOtpIntent(savedInstanceState, i)) return; _keepPasswordInOnResume = true; } else { GetIocFromLaunchIntent(i); } InitializeAfterSetIoc(); RestoreState(savedInstanceState); if (i.GetBooleanExtra("launchImmediately", false)) { App.Kp2a.GetFileStorage(_ioConnection) .PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection, RequestCodePrepareDbFile, false); } mDrawerTitle = Title; var btn = FindViewById(Resource.Id.fingerprintbtn); btn.Click += (sender, args) => { if (!string.IsNullOrEmpty((string)btn.Tag)) { 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()); b.SetOnDismissListener(new Util.DismissListener(() => _biometricDec?.StartListening(this))); b.Show(); } else _biometricDec?.StartListening(this); }; if (App.Kp2a.TrySelectCurrentDb(_ioConnection)) { //database already opened. return the ioc and we're good. LaunchNextActivity(); } Util.SetNoPersonalizedLearning(FindViewById(Resource.Id.password_edit)); } private void InitializeAfterSetIoc() { App.Kp2a.RegisterOpenAttempt(_ioConnection); InitializeFilenameView(); if (KeyProviderTypes.Contains(KeyProviders.KeyFile)) { UpdateKeyfileIocView(); } var passwordEdit = FindViewById(Resource.Id.password_edit); passwordEdit.Text = _password; var passwordFont = Typeface.CreateFromAsset(Assets, "SourceCodePro-Regular.ttf"); passwordEdit.Typeface = passwordFont; InitializePasswordModeSpinner(); UpdateOkButtonState(); } private void GetIocFromLaunchIntent(Intent i) { Kp2aLog.Log("GetIocFromLaunchIntent()"); _makeCurrent = i.GetBooleanExtra("MakeCurrent", true); Util.SetIoConnectionFromIntent(_ioConnection, i); var keyFileFromIntent = i.GetStringExtra(KeyKeyfile); if (keyFileFromIntent != null) { Kp2aLog.Log("try get keyfile from intent"); _keyFile = IOConnectionInfo.SerializeToString(IOConnectionInfo.FromPath(keyFileFromIntent)); KeyProviderTypes.Clear(); KeyProviderTypes.Add(KeyProviders.KeyFile); Kp2aLog.Log("try get keyfile from intent ok"); } else { Kp2aLog.Log("no keyprovider specified"); _keyFile = null; KeyProviderTypes.Clear(); } _password = i.GetStringExtra(KeyPassword) ?? ""; if (!KeyProviderTypes.Any()) { SetKeyProviderFromString(LoadKeyProviderStringForIoc(_ioConnection.Path)); } if ((!string.IsNullOrEmpty(_keyFile)) || (_password != "")) { _keepPasswordInOnResume = true; } } private void InitializeToolbarCollapsing() { var rootview = FindViewById(Resource.Id.main_layout); rootview.ViewTreeObserver.GlobalLayout += (sender, args2) => { Android.Util.Log.Debug("KP2A", "GlobalLayout"); if (_measureArgs == null) return; Android.Util.Log.Debug("KP2A", "ActualHeight=" + _measureArgs.ActualHeight); Android.Util.Log.Debug("KP2A", "ProposedHeight=" + _measureArgs.ProposedHeight); if (_measureArgs.ActualHeight < _measureArgs.ProposedHeight) UncollapseToolbar(); if (_measureArgs.ActualHeight > _measureArgs.ProposedHeight) CollapseToolbar(); }; rootview.MeasureEvent += (sender, args) => { _measureArgs = args; }; } private string GetKeyProviderString() { if (!KeyProviderTypes.Any()) return null; string result = Kp2aKeyProviderStringPrefix; foreach (KeyProviders type in KeyProviderTypes) { result += type.ToString(); if (type == KeyProviders.KeyFile) { result += WebUtility.UrlEncode(_keyFile) + ";"; } } return result; } private void SetKeyProviderFromString(string keyProviderString) { KeyProviderTypes.Clear(); if (string.IsNullOrEmpty(keyProviderString)) { Kp2aLog.Log("Reset keyfile"); _keyFile = null; return; } if (keyProviderString.StartsWith(Kp2aKeyProviderStringPrefix)) { keyProviderString = keyProviderString.Substring(Kp2aKeyProviderStringPrefix.Length); foreach (string type in keyProviderString.Split(';')) { Kp2aLog.Log("PasswordActivity: key file type " + type); if (!type.Trim().Any()) continue; if (type.StartsWith(KeyProviders.KeyFile.ToString())) { _keyFile = WebUtility.UrlDecode(type.Substring(KeyProviders.KeyFile.ToString().Length)); Kp2aLog.Log("Added key file of length " + _keyFile.Length); KeyProviderTypes.Add(KeyProviders.KeyFile); continue; } if (type.StartsWith(KeyProviders.ChallengeXC.ToString()+KeyProviders.KeyFile.ToString())) { _keyFile = WebUtility.UrlDecode((type.Substring(KeyProviders.ChallengeXC.ToString().Length)).Substring(KeyProviders.KeyFile.ToString().Length)); Kp2aLog.Log("Added XC key file of length " + _keyFile.Length); KeyProviderTypes.Add(KeyProviders.ChallengeXC); KeyProviderTypes.Add(KeyProviders.KeyFile); continue; } foreach (KeyProviders providerType in Enum.GetValues(typeof(KeyProviders))) { if (type == providerType.ToString()) { KeyProviderTypes.Add(providerType); break; } } } } else { Kp2aLog.Log("PasswordActivity: legacy key file mode"); //legacy mode _keyFile = null; if (keyProviderString == KeyProviderIdOtp) KeyProviderTypes.Add(KeyProviders.Otp); else if (keyProviderString == KeyProviderIdOtpRecovery) KeyProviderTypes.Add(KeyProviders.OtpRecovery); else if (keyProviderString == KeyProviderIdChallenge) KeyProviderTypes.Add(KeyProviders.Challenge); else if (keyProviderString == KeyProviderIdChallengeRecovery) KeyProviderTypes.Add(KeyProviders.ChalRecovery); else if (keyProviderString == KeyProviderIdChallengeXC) KeyProviderTypes.Add(KeyProviders.ChallengeXC); else { KeyProviderTypes.Add(KeyProviders.KeyFile); _keyFile = keyProviderString; } if (KeyProviderTypes.Contains(KeyProviders.KeyFile)) { //test if the filename is properly encoded. try { Kp2aLog.Log("test if stored filename is ok"); IOConnectionInfo.UnserializeFromString(_keyFile); Kp2aLog.Log("...ok"); } catch (Exception e) { //it's not. This is probably because we're upgrading from app version <= 45 //where the keyfile was stored plain text and not serialized Kp2aLog.Log("no, it's not: " + e.GetType().Name); var serializedKeyFile = IOConnectionInfo.SerializeToString(IOConnectionInfo.FromPath(_keyFile)); Kp2aLog.Log("now it is!"); _keyFile = serializedKeyFile; } } } } private void ClearFingerprintUnlockData() { ISharedPreferencesEditor edit = PreferenceManager.GetDefaultSharedPreferences(this).Edit(); edit.PutString(Database.GetFingerprintPrefKey(_ioConnection), ""); edit.PutString(Database.GetFingerprintModePrefKey(_ioConnection), FingerprintUnlockMode.Disabled.ToString()); edit.Commit(); } public void OnBiometricError(string 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) { //ignore } public void OnBiometricAuthSucceeded() { var btn = FindViewById(Resource.Id.fingerprintbtn); btn.SetImageResource(Resource.Drawable.ic_fingerprint_success); try { var masterPassword = _biometricDec.DecryptStored(Database.GetFingerprintPrefKey(_ioConnection)); //first mask the password textedit before assigning the password: if (_showPassword) { _showPassword = false; MakePasswordMaskedOrVisible(); } _password = FindViewById(Resource.Id.password_edit).Text = masterPassword; FindViewById(Resource.Id.password_edit).Enabled = false; //prevent accidental modification of password } catch (Java.Security.GeneralSecurityException ex) { Kp2aLog.Log("GeneralSecurityException in DecryptStored"); Kp2aLog.LogUnexpectedError(ex); HandleFingerprintKeyInvalidated(); return; } btn.PostDelayed(() => { //fire if everything else is ready if (FindViewById(Resource.Id.pass_ok).Enabled) { OnOk(true); } FindViewById(Resource.Id.password_edit).Enabled = true; }, 500); } private void InitializeNavDrawerButtons() { FindViewById(Resource.Id.btn_nav_change_db).Click += (sender, args) => { GoToFileSelectActivity(); }; FindViewById(Resource.Id.btn_nav_donate).Click += (sender, args) => { Util.GotoDonateUrl(this); }; FindViewById(Resource.Id.btn_nav_donate).Visibility = PreferenceManager.GetDefaultSharedPreferences(this) .GetBoolean(GetString(Resource.String.NoDonateOption_key), false) ? ViewStates.Gone : ViewStates.Visible; FindViewById(Resource.Id.btn_nav_about).Click += (sender, args) => { AboutDialog dialog = new AboutDialog(this); dialog.Show(); }; FindViewById(Resource.Id.btn_nav_settings).Click += (sender, args) => { AppSettingsActivity.Launch(this); }; FindViewById(Resource.Id.nav_app_name).Click += (sender, args) => { _appnameclickCount++; if (_appnameclickCount == 6) { Kp2aLog.LogUnexpectedError(new Exception("some blabla")); App.Kp2a.ShowMessage(this, "Once again and the app will crash.", MessageSeverity.Warning); } if (_appnameclickCount == 7) { throw new Exception("this is an easter egg crash (to test uncaught exceptions.)"); } }; } private void InitializeToolbar() { var collapsingToolbar = FindViewById(Resource.Id.collapsing_toolbar); collapsingToolbar.SetTitle(GetString(Resource.String.unlock_database_title)); _drawerLayout = FindViewById(Resource.Id.drawer_layout); mDrawerToggle = new ActionBarDrawerToggle(this, _drawerLayout, Resource.String.menu_open, Resource.String.menu_close); _drawerLayout?.SetDrawerListener(mDrawerToggle); SetSupportActionBar(FindViewById(Resource.Id.toolbar)); SupportActionBar.SetDisplayHomeAsUpEnabled(true); SupportActionBar.SetHomeButtonEnabled(true); mDrawerToggle.SyncState(); } public override void OnBackPressed() { if (_drawerLayout != null) { if (_drawerLayout.IsDrawerOpen((int) GravityFlags.Start)) { _drawerLayout.CloseDrawer((int) GravityFlags.Start); return; } } base.OnBackPressed(); } private void InitializeOtpSecretSpinner() { Spinner spinner = FindViewById(Resource.Id.otpsecret_format_spinner); ArrayAdapter spinnerArrayAdapter = new ArrayAdapter(this, Android.Resource.Layout.SimpleSpinnerDropDownItem, EncodingUtil.Formats); spinner.Adapter = spinnerArrayAdapter; } private bool GetIocFromOtpIntent(Bundle savedInstanceState, Intent i) { //create called after detecting an OTP via NFC //this means the Activity was not on the back stack before, i.e. no database has been selected _ioConnection = null; //see if we can get a database from recent: if (App.Kp2a.FileDbHelper.HasRecentFiles()) { ICursor filesCursor = App.Kp2a.FileDbHelper.FetchAllFiles(); StartManagingCursor(filesCursor); filesCursor.MoveToFirst(); IOConnectionInfo ioc = App.Kp2a.FileDbHelper.CursorToIoc(filesCursor); if (App.Kp2a.GetFileStorage(ioc).RequiresSetup(ioc) == false) { IFileStorage fileStorage = App.Kp2a.GetFileStorage(ioc); if (!fileStorage.RequiresCredentials(ioc)) { //ok, we can use this file _ioConnection = ioc; } } } if (_ioConnection == null) { //We need to go to FileSelectActivity first. //For security reasons: discard the OTP (otherwise the user might not select a database now and forget //about the OTP, but it would still be stored in the Intents and later be passed to PasswordActivity again. App.Kp2a.ShowMessage(this, GetString(Resource.String.otp_discarded_because_no_db), MessageSeverity.Warning); GoToFileSelectActivity(); return false; } //assume user wants to use OTP (for static password, they need to open KP2A first and select the key provider type, then see OnNewIntent) KeyProviderTypes.Clear(); KeyProviderTypes.Add(KeyProviders.Otp); if (savedInstanceState == null) //only when not re-creating { //remember the OTP for later use _pendingOtps.Add(i.GetStringExtra(Intents.OtpExtraKey)); i.RemoveExtra(Intents.OtpExtraKey); } return true; } private void InitializeBottomBarButtons() { Button confirmButton = (Button) FindViewById(Resource.Id.pass_ok); confirmButton.Click += (sender, e) => { OnOk(); }; var changeDbButton = FindViewById