2313 lines
76 KiB
C#
2313 lines
76 KiB
C#
/*
|
|
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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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<MemoryStream> _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<String> _pendingOtps = new List<string>();
|
|
|
|
|
|
private HashSet<KeyProviders> KeyProviderTypes = new HashSet<KeyProviders>();
|
|
|
|
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<object, object, object>(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<TextView>(Resource.Id.label_keyfilename).Tag = _keyFile;
|
|
if (string.IsNullOrEmpty(_keyFile))
|
|
{
|
|
FindViewById<TextView>(Resource.Id.filestorage_label).Visibility = ViewStates.Gone;
|
|
FindViewById<ImageView>(Resource.Id.filestorage_logo).Visibility = ViewStates.Gone;
|
|
FindViewById<TextView>(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<ImageView>(Resource.Id.filestorage_logo).SetImageDrawable(drawable);
|
|
FindViewById<ImageView>(Resource.Id.filestorage_logo).Visibility = ViewStates.Visible;
|
|
|
|
|
|
String title = App.Kp2a.GetStorageDisplayName(protocolId);
|
|
FindViewById<TextView>(Resource.Id.filestorage_label).Text = title;
|
|
FindViewById<TextView>(Resource.Id.filestorage_label).Visibility = ViewStates.Visible;
|
|
|
|
FindViewById<TextView>(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<object, object, object>(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<string> 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<string> 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<EditText>(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<AppBarLayout>(Resource.Id.appbar);
|
|
var tmp = appbarLayout.LayoutParameters;
|
|
CoordinatorLayout.LayoutParams p = tmp.JavaCast<CoordinatorLayout.LayoutParams>();
|
|
var tmp2 = p.Behavior;
|
|
var behavior = tmp2.JavaCast<AppBarLayout.Behavior>();
|
|
if (behavior == null)
|
|
{
|
|
p.Behavior = behavior = new AppBarLayout.Behavior();
|
|
}
|
|
behavior.OnNestedFling(FindViewById<CoordinatorLayout>(Resource.Id.main_content), appbarLayout, null, 0, -10000, false);
|
|
}
|
|
|
|
private void CollapseToolbar()
|
|
{
|
|
AppBarLayout appbarLayout = FindViewById<AppBarLayout>(Resource.Id.appbar);
|
|
ViewGroup.LayoutParams tmp = appbarLayout.LayoutParameters;
|
|
CoordinatorLayout.LayoutParams p = tmp.JavaCast<CoordinatorLayout.LayoutParams>();
|
|
var tmp2 = p.Behavior;
|
|
var behavior = tmp2.JavaCast<AppBarLayout.Behavior>();
|
|
if (behavior == null)
|
|
{
|
|
p.Behavior = behavior = new AppBarLayout.Behavior();
|
|
}
|
|
behavior.OnNestedFling(FindViewById<CoordinatorLayout>(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<EditText>(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<EditText>(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<EditText>(Resource.Id.password_edit));
|
|
|
|
|
|
}
|
|
|
|
private void InitializeAfterSetIoc()
|
|
{
|
|
App.Kp2a.RegisterOpenAttempt(_ioConnection);
|
|
|
|
|
|
InitializeFilenameView();
|
|
|
|
if (KeyProviderTypes.Contains(KeyProviders.KeyFile))
|
|
{
|
|
UpdateKeyfileIocView();
|
|
}
|
|
|
|
var passwordEdit = FindViewById<EditText>(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<MeasuringLinearLayout>(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<ImageButton>(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<ImageButton>(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<EditText>(Resource.Id.password_edit).Text = masterPassword;
|
|
|
|
FindViewById<EditText>(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<EditText>(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<CollapsingToolbarLayout>(Resource.Id.collapsing_toolbar);
|
|
collapsingToolbar.SetTitle(GetString(Resource.String.unlock_database_title));
|
|
|
|
_drawerLayout = FindViewById<DrawerLayout>(Resource.Id.drawer_layout);
|
|
mDrawerToggle = new ActionBarDrawerToggle(this, _drawerLayout,
|
|
Resource.String.menu_open,
|
|
Resource.String.menu_close);
|
|
|
|
|
|
_drawerLayout?.SetDrawerListener(mDrawerToggle);
|
|
|
|
SetSupportActionBar(FindViewById<Toolbar>(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<Spinner>(Resource.Id.otpsecret_format_spinner);
|
|
ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<String>(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<Button>(Resource.Id.change_db);
|
|
string label = changeDbButton.Text;
|
|
if (label.EndsWith("\u2026"))
|
|
changeDbButton.Text = label.Substring(0, label.Length - 1);
|
|
changeDbButton.Click += (sender, args) => GoToFileSelectActivity();
|
|
|
|
Util.MoveBottomBarButtons(Resource.Id.change_db, Resource.Id.pass_ok, Resource.Id.bottom_bar, this);
|
|
}
|
|
|
|
private void OnOk(bool usedFingerprintUnlock = false)
|
|
{
|
|
UsedFingerprintUnlock = usedFingerprintUnlock;
|
|
App.Kp2a.GetFileStorage(_ioConnection)
|
|
.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection,
|
|
RequestCodePrepareDbFile, false);
|
|
}
|
|
|
|
|
|
private void InitializeTogglePasswordButton()
|
|
{
|
|
ImageButton btnTogglePassword = (ImageButton) FindViewById(Resource.Id.toggle_password);
|
|
btnTogglePassword.Click += (sender, e) =>
|
|
{
|
|
_showPassword = !_showPassword;
|
|
MakePasswordMaskedOrVisible();
|
|
};
|
|
Android.Graphics.PorterDuff.Mode mMode = Android.Graphics.PorterDuff.Mode.SrcAtop;
|
|
Color color = new Color (224, 224, 224);
|
|
btnTogglePassword.SetColorFilter (color, mMode);
|
|
}
|
|
|
|
private void InitializeKeyfileBrowseButton()
|
|
{
|
|
var browseButton = (Button)FindViewById(Resource.Id.btn_change_location);
|
|
browseButton.Click += (sender, evt) =>
|
|
{
|
|
Intent intent = new Intent(this, typeof(SelectStorageLocationActivity));
|
|
intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppGet, true);
|
|
intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, false);
|
|
intent.PutExtra(FileStorageSetupDefs.ExtraIsForSave, false);
|
|
StartActivityForResult(intent, RequestCodeSelectKeyfile);
|
|
};
|
|
}
|
|
|
|
private void InitializePasswordModeSpinner()
|
|
{
|
|
Spinner passwordModeSpinner = FindViewById<Spinner>(Resource.Id.password_mode_spinner);
|
|
if (passwordModeSpinner != null)
|
|
{
|
|
UpdateKeyProviderUiState();
|
|
|
|
int spinnerPos = 0;
|
|
if (KeyProviderTypes.Contains(KeyProviders.KeyFile))
|
|
{
|
|
if (KeyProviderTypes.Contains(KeyProviders.ChallengeXC))
|
|
spinnerPos = 7;
|
|
else spinnerPos = 1;
|
|
}
|
|
else if (KeyProviderTypes.Contains(KeyProviders.Otp))
|
|
spinnerPos = 2;
|
|
else if (KeyProviderTypes.Contains(KeyProviders.OtpRecovery))
|
|
spinnerPos = 3;
|
|
else if (KeyProviderTypes.Contains(KeyProviders.Challenge))
|
|
spinnerPos = 4;
|
|
else if (KeyProviderTypes.Contains(KeyProviders.ChalRecovery))
|
|
spinnerPos = 5;
|
|
else if (KeyProviderTypes.Contains(KeyProviders.ChallengeXC))
|
|
spinnerPos = 6;
|
|
|
|
passwordModeSpinner.SetSelection(spinnerPos);
|
|
passwordModeSpinner.ItemSelected += (sender, args) =>
|
|
{
|
|
KeyProviderTypes.Clear();
|
|
_keyFile = null;
|
|
Kp2aLog.Log("PasswordModeSpinner item selected: " + args.Position);
|
|
switch (args.Position)
|
|
{
|
|
case 0:
|
|
break;
|
|
case 1:
|
|
//don't set to "" to prevent losing the filename. (ItemSelected is also called during recreation!)
|
|
Kp2aLog.Log("key file length before: " + _keyFile?.Length);
|
|
_keyFile = (FindViewById(Resource.Id.label_keyfilename).Tag ?? "").ToString();
|
|
Kp2aLog.Log("key file length after: " + _keyFile?.Length);
|
|
KeyProviderTypes.Add(KeyProviders.KeyFile);
|
|
break;
|
|
case 2:
|
|
KeyProviderTypes.Add(KeyProviders.Otp);
|
|
break;
|
|
case 3:
|
|
KeyProviderTypes.Add(KeyProviders.OtpRecovery);
|
|
break;
|
|
case 4:
|
|
KeyProviderTypes.Add(KeyProviders.Challenge);
|
|
break;
|
|
case 5:
|
|
KeyProviderTypes.Add(KeyProviders.ChalRecovery);
|
|
break;
|
|
case 6:
|
|
KeyProviderTypes.Add(KeyProviders.ChallengeXC);
|
|
break;
|
|
case 7:
|
|
//don't set to "" to prevent losing the filename. (ItemSelected is also called during recreation!)
|
|
Kp2aLog.Log("key file length before: " + _keyFile?.Length);
|
|
_keyFile = (FindViewById(Resource.Id.label_keyfilename).Tag ?? "").ToString();
|
|
Kp2aLog.Log("key file length after: " + _keyFile?.Length);
|
|
KeyProviderTypes.Add(KeyProviders.ChallengeXC);
|
|
KeyProviderTypes.Add(KeyProviders.KeyFile);
|
|
break;
|
|
default:
|
|
throw new Exception("Unexpected position " + args.Position + " / " +
|
|
((ICursor) ((AdapterView) sender).GetItemAtPosition(args.Position)).GetString(1));
|
|
}
|
|
UpdateKeyProviderUiState();
|
|
};
|
|
FindViewById(Resource.Id.init_otp).Click += (sender, args) =>
|
|
{
|
|
App.Kp2a.GetOtpAuxFileStorage(_ioConnection)
|
|
.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection,
|
|
RequestCodePrepareOtpAuxFile, false);
|
|
};
|
|
}
|
|
}
|
|
|
|
private void RestoreState(Bundle savedInstanceState)
|
|
{
|
|
if (savedInstanceState != null)
|
|
{
|
|
Kp2aLog.Log("PasswordActivity: Restoring state from savedInstanceState");
|
|
_showPassword = savedInstanceState.GetBoolean(ShowpasswordKey, false);
|
|
MakePasswordMaskedOrVisible();
|
|
|
|
if (!string.IsNullOrEmpty(savedInstanceState.GetString(KeyFileOrProviderKey)))
|
|
Kp2aLog.Log("No key provider found");
|
|
else
|
|
Kp2aLog.Log("Key provider found");
|
|
SetKeyProviderFromString(savedInstanceState.GetString(KeyFileOrProviderKey));
|
|
_password = FindViewById<EditText>(Resource.Id.password_edit).Text = savedInstanceState.GetString(PasswordKey);
|
|
|
|
_pendingOtps = new List<string>(savedInstanceState.GetStringArrayList(PendingOtpsKey));
|
|
|
|
string otpInfoString = savedInstanceState.GetString(OtpInfoKey);
|
|
if (otpInfoString != null)
|
|
{
|
|
|
|
XmlSerializer xs = new XmlSerializer(typeof(OtpInfo));
|
|
|
|
XmlReaderSettings settings = new XmlReaderSettings() { XmlResolver = null, DtdProcessing = DtdProcessing.Ignore };
|
|
var reader = XmlReader.Create(new StringReader(otpInfoString), settings);
|
|
|
|
_otpInfo = (OtpInfo)xs.Deserialize(reader);
|
|
|
|
var enteredOtps = savedInstanceState.GetStringArrayList(EnteredOtpsKey);
|
|
|
|
ShowOtpEntry(enteredOtps);
|
|
}
|
|
|
|
UpdateKeyProviderUiState();
|
|
|
|
}
|
|
}
|
|
|
|
private void UpdateOkButtonState()
|
|
{
|
|
bool enabled = true;
|
|
if (KeyProviderTypes.Contains(KeyProviders.KeyFile))
|
|
enabled &= _keyFile != "" || _password != "";
|
|
|
|
if (KeyProviderTypes.Contains(KeyProviders.Otp))
|
|
{
|
|
if (_otpInfo == null)
|
|
enabled = false;
|
|
else
|
|
{
|
|
int c = 0;
|
|
foreach (int otpId in _otpTextViewIds)
|
|
{
|
|
c++;
|
|
var otpTextView = FindViewById<EditText>(otpId);
|
|
if ((c <= _otpInfo.OtpsRequired) && (otpTextView.Text == ""))
|
|
{
|
|
enabled = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (KeyProviderTypes.Contains(KeyProviders.OtpRecovery) || KeyProviderTypes.Contains(KeyProviders.ChalRecovery))
|
|
{
|
|
enabled &= FindViewById<EditText>(Resource.Id.pass_otpsecret).Text != "";
|
|
}
|
|
|
|
if (KeyProviderTypes.Contains(KeyProviders.Challenge))
|
|
enabled &= _challengeSecret != null;
|
|
|
|
FindViewById(Resource.Id.pass_ok).Enabled = enabled;
|
|
|
|
}
|
|
|
|
private void UpdateKeyProviderUiState()
|
|
{
|
|
FindViewById(Resource.Id.keyfileLine).Visibility = KeyProviderHasKeyFile()
|
|
? ViewStates.Visible
|
|
: ViewStates.Gone;
|
|
if (KeyProviderHasKeyFile())
|
|
{
|
|
UpdateKeyfileIocView();
|
|
}
|
|
|
|
FindViewById(Resource.Id.otpView).Visibility = KeyProviderTypes.Contains(KeyProviders.Otp)
|
|
? ViewStates.Visible
|
|
: ViewStates.Gone;
|
|
|
|
FindViewById(Resource.Id.otpSecretLine).Visibility = (KeyProviderTypes.Contains(KeyProviders.OtpRecovery) || KeyProviderTypes.Contains(KeyProviders.ChalRecovery))
|
|
? ViewStates.Visible
|
|
: ViewStates.Gone;
|
|
if (KeyProviderTypes.Contains(KeyProviders.Otp))
|
|
{
|
|
FindViewById(Resource.Id.otps_pending).Visibility = _pendingOtps.Count > 0 ? ViewStates.Visible : ViewStates.Gone;
|
|
}
|
|
|
|
if (KeyProviderTypes.Contains(KeyProviders.Challenge))
|
|
{
|
|
FindViewById (Resource.Id.otpView).Visibility = ViewStates.Visible;
|
|
FindViewById(Resource.Id.otps_pending).Visibility = ViewStates.Gone;
|
|
}
|
|
UpdateOkButtonState();
|
|
}
|
|
|
|
private bool KeyProviderHasKeyFile()
|
|
{
|
|
return KeyProviderTypes.Contains(KeyProviders.KeyFile);
|
|
}
|
|
|
|
private void PerformLoadDatabase()
|
|
{
|
|
if (_performingLoad)
|
|
return;
|
|
CurrentlyWaitingKey = null;
|
|
_performingLoad = true;
|
|
//put loading into background thread to allow loading the key file (potentially over network)
|
|
new SimpleLoadingDialog(this, GetString(Resource.String.loading),
|
|
true, () =>
|
|
{
|
|
CompositeKey compositeKey;
|
|
string errorMessage;
|
|
if (!CreateCompositeKey(out compositeKey, out errorMessage)) return (() =>
|
|
{
|
|
App.Kp2a.ShowMessage(this, errorMessage, MessageSeverity.Warning);
|
|
_performingLoad = false;
|
|
});
|
|
return () => { PerformLoadDatabaseWithCompositeKey(compositeKey); };
|
|
}).Execute();
|
|
|
|
|
|
}
|
|
|
|
private void PerformLoadDatabaseWithCompositeKey(CompositeKey compositeKey)
|
|
{
|
|
CheckBox cbQuickUnlock = (CheckBox) FindViewById(Resource.Id.enable_quickunlock);
|
|
if (cbQuickUnlock == null)
|
|
throw new NullPointerException("cpQuickUnlock");
|
|
App.Kp2a.SetQuickUnlockEnabled(cbQuickUnlock.Checked);
|
|
App.Kp2a.ScreenLockWasEnabledWhenOpeningDatabase =
|
|
(((KeyguardManager)GetSystemService(Context.KeyguardService)!)!).IsDeviceSecure;
|
|
App.Kp2a.QuickUnlockBlockedWhenDeviceNotSecureWhenOpeningDatabase = PreferenceManager.GetDefaultSharedPreferences(this)
|
|
.GetBoolean(GetString(Resource.String.QuickUnlockBlockedWhenDeviceNotSecure_key), true);
|
|
|
|
if ((_loadDbFileTask != null) && (App.Kp2a.OfflineMode != _loadDbTaskOffline))
|
|
{
|
|
if (App.Kp2a == null)
|
|
throw new NullPointerException("App.Kp2a");
|
|
//keep the loading result if we loaded in online-mode (now offline) and the task is completed
|
|
if (!App.Kp2a.OfflineMode || !_loadDbFileTask.IsCompleted)
|
|
{
|
|
//discard the pre-loading task
|
|
_loadDbFileTask = null;
|
|
}
|
|
|
|
}
|
|
|
|
//avoid password being visible while loading:
|
|
_showPassword = false;
|
|
try
|
|
{
|
|
MakePasswordMaskedOrVisible();
|
|
|
|
Handler handler = new Handler();
|
|
OnFinish onFinish = new AfterLoad(handler, this, _ioConnection);
|
|
LoadDb task = (KeyProviderTypes.Contains(KeyProviders.Otp))
|
|
? new SaveOtpAuxFileAndLoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, GetKeyProviderString(),
|
|
onFinish, this, true, _makeCurrent)
|
|
: new LoadDb(this, App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, GetKeyProviderString(), onFinish,true, _makeCurrent);
|
|
_loadDbFileTask = null; // prevent accidental re-use
|
|
|
|
new ProgressTask(App.Kp2a, this, task).Run();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Kp2aLog.LogUnexpectedError(new Exception("cannot load database: "+e + ", c: " + (compositeKey != null) + (_ioConnection != null) + (_keyFile != null), e));
|
|
throw;
|
|
}
|
|
|
|
}
|
|
|
|
private bool CreateCompositeKey(out CompositeKey compositeKey, out string errorMessage)
|
|
{
|
|
errorMessage = null;
|
|
//no need to check for validity of password because if this method is called, the Ok button was enabled (i.e. there was a valid password)
|
|
compositeKey = new CompositeKey();
|
|
compositeKey.AddUserKey(new KcpPassword(_password));
|
|
if (KeyProviderTypes.Contains(KeyProviders.KeyFile))
|
|
{
|
|
try
|
|
{
|
|
if (_keyFile == "")
|
|
throw new System.IO.FileNotFoundException();
|
|
var ioc = IOConnectionInfo.UnserializeFromString(_keyFile);
|
|
using (var stream = App.Kp2a.GetFileStorage(ioc).OpenFileForRead(ioc))
|
|
{
|
|
byte[] keyfileData = Util.StreamToMemoryStream(stream).ToArray();
|
|
compositeKey.AddUserKey(new KcpKeyFile(keyfileData, ioc, true));
|
|
}
|
|
}
|
|
catch (System.IO.FileNotFoundException e)
|
|
{
|
|
Kp2aLog.Log(e.ToString());
|
|
errorMessage = App.Kp2a.GetResourceString(UiStringKey.keyfile_does_not_exist);
|
|
return false;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Kp2aLog.LogUnexpectedError(e);
|
|
errorMessage = Util.GetErrorMessage(e);
|
|
return false;
|
|
}
|
|
}
|
|
if (KeyProviderTypes.Contains(KeyProviders.Otp))
|
|
{
|
|
try
|
|
{
|
|
var lOtps = GetOtpsFromUi();
|
|
Kp2aLog.Log("received " + lOtps.Count + " otps.");
|
|
OathHotpKeyProv.CreateOtpSecret(lOtps, _otpInfo);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Kp2aLog.LogUnexpectedError(e);
|
|
errorMessage = GetString(Resource.String.OtpKeyError);
|
|
|
|
return false;
|
|
}
|
|
compositeKey.AddUserKey(new KcpCustomKey(OathHotpKeyProv.Name, _otpInfo.Secret, true));
|
|
}
|
|
if ((KeyProviderTypes.Contains(KeyProviders.OtpRecovery)) || (KeyProviderTypes.Contains(KeyProviders.ChalRecovery)))
|
|
{
|
|
Spinner stpDataFmtSpinner = FindViewById<Spinner>(Resource.Id.otpsecret_format_spinner);
|
|
EditText secretEdit = FindViewById<EditText>(Resource.Id.pass_otpsecret);
|
|
|
|
byte[] pbSecret = EncodingUtil.ParseKey(secretEdit.Text, (OtpDataFmt) stpDataFmtSpinner.SelectedItemPosition);
|
|
if (pbSecret != null)
|
|
{
|
|
compositeKey.AddUserKey(new KcpCustomKey(OathHotpKeyProv.Name, pbSecret, true));
|
|
}
|
|
else
|
|
{
|
|
errorMessage = GetString(Resource.String.CouldntParseOtpSecret);
|
|
return false;
|
|
}
|
|
}
|
|
if (KeyProviderTypes.Contains(KeyProviders.Challenge))
|
|
{
|
|
compositeKey.AddUserKey(new KcpCustomKey(KeeChallengeProv.Name, _challengeSecret, true));
|
|
}
|
|
if (KeyProviderTypes.Contains(KeyProviders.ChallengeXC))
|
|
{
|
|
CurrentlyWaitingKey = new ChallengeXCKey(this, RequestCodeChallengeYubikey);
|
|
compositeKey.AddUserKey(CurrentlyWaitingKey);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private List<string> GetOtpsFromUi()
|
|
{
|
|
List<string> lOtps = new List<string>();
|
|
foreach (int otpId in _otpTextViewIds)
|
|
{
|
|
string otpText = FindViewById<EditText>(otpId).Text;
|
|
if (!String.IsNullOrEmpty(otpText))
|
|
lOtps.Add(otpText);
|
|
}
|
|
return lOtps;
|
|
}
|
|
|
|
|
|
private void MakePasswordMaskedOrVisible()
|
|
{
|
|
EditText password = (EditText) FindViewById(Resource.Id.password_edit);
|
|
int selStart = password.SelectionStart, selEnd = password.SelectionEnd;
|
|
if (_showPassword)
|
|
{
|
|
password.InputType = InputTypes.ClassText | InputTypes.TextVariationVisiblePassword;
|
|
_passwordFont.ApplyTo(password);
|
|
}
|
|
else
|
|
{
|
|
password.InputType = InputTypes.ClassText | InputTypes.TextVariationPassword;
|
|
}
|
|
password.SetSelection(selStart, selEnd);
|
|
}
|
|
|
|
protected override void OnPause()
|
|
{
|
|
_biometricDec?.StopListening();
|
|
_lastOnPauseTime = DateTime.Now;
|
|
|
|
base.OnPause();
|
|
}
|
|
|
|
private bool hasRequestedKeyboardActivation = false;
|
|
|
|
|
|
protected override void OnStart()
|
|
{
|
|
base.OnStart();
|
|
_starting = true;
|
|
|
|
if (PreferenceManager.GetDefaultSharedPreferences(this)
|
|
.GetBoolean(GetString(Resource.String.UseKp2aKeyboardInKp2a_key), false)
|
|
&& !hasRequestedKeyboardActivation)
|
|
{
|
|
hasRequestedKeyboardActivation = true;
|
|
CopyToClipboardService.ActivateKeyboard(this);
|
|
}
|
|
|
|
DonateReminder.ShowDonateReminderIfAppropriate(this);
|
|
|
|
|
|
if (compositeKeyForImmediateLoad == null && !fingerprintInitialized)
|
|
{
|
|
fingerprintInitialized = InitFingerprintUnlock();
|
|
}
|
|
|
|
}
|
|
|
|
private MemoryStream PreloadDbFile()
|
|
{
|
|
Kp2aLog.Log("Pre-loading database file starting");
|
|
var fileStorage = App.Kp2a.GetFileStorage(_ioConnection);
|
|
var stream = fileStorage.OpenFileForRead(_ioConnection);
|
|
|
|
var memoryStream = Util.StreamToMemoryStream(stream);
|
|
|
|
Kp2aLog.Log("Pre-loading database file completed");
|
|
|
|
return memoryStream;
|
|
}
|
|
|
|
protected override void OnSaveInstanceState(Bundle outState)
|
|
{
|
|
base.OnSaveInstanceState(outState);
|
|
outState.PutBoolean(ShowpasswordKey, _showPassword);
|
|
|
|
outState.PutString(KeyFileOrProviderKey, GetKeyProviderString());
|
|
outState.PutString(PasswordKey, _password);
|
|
outState.PutStringArrayList(PendingOtpsKey, _pendingOtps);
|
|
if (_otpInfo != null)
|
|
{
|
|
outState.PutStringArrayList(EnteredOtpsKey, GetOtpsFromUi());
|
|
|
|
var sw = new StringWriter();
|
|
|
|
var xws = OtpInfo.XmlWriterSettings();
|
|
|
|
XmlWriter xw = XmlWriter.Create(sw, xws);
|
|
|
|
XmlSerializer xs = new XmlSerializer(typeof(OtpInfo));
|
|
xs.Serialize(xw, _otpInfo);
|
|
|
|
xw.Close();
|
|
|
|
outState.PutString(OtpInfoKey, sw.ToString());
|
|
}
|
|
|
|
}
|
|
|
|
protected override void OnNewIntent(Intent intent)
|
|
{
|
|
base.OnNewIntent(intent);
|
|
|
|
//this method is called from the NfcOtpActivity's startActivity() if the activity is already running
|
|
//note: it's not called in other cases because OnNewIntent requires the activity to be on top already
|
|
//which is never the case when started from another activity (in the same task).
|
|
//NfcOtpActivity sets the ClearTop flag to get OnNewIntent called.
|
|
if (intent != null)
|
|
{
|
|
if (intent.HasExtra(Intents.OtpExtraKey))
|
|
{
|
|
string otp = intent.GetStringExtra(Intents.OtpExtraKey);
|
|
_keepPasswordInOnResume = true;
|
|
if (KeyProviderTypes.Contains(KeyProviders.Otp))
|
|
{
|
|
|
|
if (_otpInfo == null)
|
|
{
|
|
//Entering OTPs not yet initialized:
|
|
_pendingOtps.Add(otp);
|
|
UpdateKeyProviderUiState();
|
|
}
|
|
else
|
|
{
|
|
//Entering OTPs is initialized. Write OTP into first empty field:
|
|
bool foundEmptyField = false;
|
|
foreach (int otpId in _otpTextViewIds)
|
|
{
|
|
EditText otpEdit = FindViewById<EditText>(otpId);
|
|
if ((otpEdit.Visibility == ViewStates.Visible) && String.IsNullOrEmpty(otpEdit.Text))
|
|
{
|
|
otpEdit.Text = otp;
|
|
foundEmptyField = true;
|
|
break;
|
|
}
|
|
}
|
|
//did we find a field?
|
|
if (!foundEmptyField)
|
|
{
|
|
App.Kp2a.ShowMessage(this, GetString(Resource.String.otp_discarded_no_space), MessageSeverity.Error);
|
|
}
|
|
}
|
|
|
|
Spinner passwordModeSpinner = FindViewById<Spinner>(Resource.Id.password_mode_spinner);
|
|
if (passwordModeSpinner.SelectedItemPosition != (int)KeyProviders.Otp)
|
|
{
|
|
passwordModeSpinner.SetSelection((int)KeyProviders.Otp);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//assume the key should be used as static password
|
|
FindViewById<EditText>(Resource.Id.password_edit).Text += otp;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// if the activity is launched twice and the first initialization hasn't even finished, we cannot
|
|
// reset the state and re-initialize the activity.
|
|
// This can happen with autofill in some cases (#2869)
|
|
if (_resumeCompleted)
|
|
{
|
|
ResetState();
|
|
GetIocFromLaunchIntent(intent);
|
|
InitializeAfterSetIoc();
|
|
OnStart();
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private void ResetState()
|
|
{
|
|
_makeCurrent = false;
|
|
_loadDbFileTask = null;
|
|
_loadDbTaskOffline = false;
|
|
|
|
_showPassword = false;
|
|
MakePasswordMaskedOrVisible();
|
|
_killOnDestroy = false;
|
|
_password = "";
|
|
_pendingOtps.Clear();
|
|
KeyProviderTypes.Clear();
|
|
_rememberKeyfile = false;
|
|
_starting = false;
|
|
_otpInfo = null;
|
|
_otpAuxIoc = null;
|
|
_chalInfo = null;
|
|
_challengeSecret = null;
|
|
_challengeProv = null;
|
|
_performingLoad = false;
|
|
_keepPasswordInOnResume = false;
|
|
_lastOnPauseTime = DateTime.MinValue;
|
|
mDrawerTitle = "";
|
|
_measureArgs = null;
|
|
_biometricDec = null;
|
|
_appnameclickCount = 0;
|
|
InvalidCompositeKeyCount = 0;
|
|
LoadingErrorCount = 0;
|
|
fingerprintInitialized = false;
|
|
UsedFingerprintUnlock = false;
|
|
}
|
|
|
|
protected override View? SnackbarAnchorView => FindViewById(Resource.Id.main_content);
|
|
|
|
protected override void OnResume()
|
|
{
|
|
base.OnResume();
|
|
|
|
_activityDesign.ReapplyTheme();
|
|
|
|
Kp2aLog.Log("starting: " + _starting + ", Finishing: " + IsFinishing + ", _performingLoad: " +
|
|
_performingLoad);
|
|
|
|
CheckBox cbOfflineMode = (CheckBox)FindViewById(Resource.Id.work_offline);
|
|
App.Kp2a.OfflineMode =
|
|
cbOfflineMode.Checked =
|
|
App.Kp2a
|
|
.OfflineModePreference; //this won't overwrite new user settings because every change is directly saved in settings
|
|
LinearLayout offlineModeContainer = FindViewById<LinearLayout>(Resource.Id.work_offline_container);
|
|
var cachingFileStorage = App.Kp2a.GetFileStorage(_ioConnection) as CachingFileStorage;
|
|
if ((cachingFileStorage != null) && cachingFileStorage.IsCached(_ioConnection))
|
|
{
|
|
offlineModeContainer.Visibility = ViewStates.Visible;
|
|
}
|
|
else
|
|
{
|
|
offlineModeContainer.Visibility = ViewStates.Gone;
|
|
App.Kp2a.OfflineMode = false;
|
|
}
|
|
|
|
|
|
|
|
|
|
View killButton = FindViewById(Resource.Id.kill_app);
|
|
if (PreferenceManager.GetDefaultSharedPreferences(this)
|
|
.GetBoolean(GetString(Resource.String.show_kill_app_key), false))
|
|
{
|
|
killButton.Click += (sender, args) =>
|
|
{
|
|
_killOnDestroy = true;
|
|
SetResult(Result.Canceled);
|
|
Finish();
|
|
|
|
};
|
|
killButton.Visibility = ViewStates.Visible;
|
|
|
|
}
|
|
else
|
|
{
|
|
killButton.Visibility = ViewStates.Gone;
|
|
}
|
|
|
|
TryGetOtpFromClipboard();
|
|
|
|
if (!_keepPasswordInOnResume)
|
|
{
|
|
if (
|
|
_lastOnPauseTime <
|
|
DateTime.Now -
|
|
TimeSpan.FromSeconds(
|
|
5) //only clear when user left the app for more than 5 seconds (allows to use Yubiclip, also allows to switch shortly to another app)
|
|
&&
|
|
PreferenceManager.GetDefaultSharedPreferences(this)
|
|
.GetBoolean(GetString(Resource.String.ClearPasswordOnLeave_key), true))
|
|
{
|
|
ClearEnteredPassword();
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_keepPasswordInOnResume = false;
|
|
|
|
MakePasswordMaskedOrVisible();
|
|
|
|
UpdateOkButtonState();
|
|
|
|
if (KeyProviderTypes.Contains(KeyProviders.Challenge))
|
|
{
|
|
FindViewById(Resource.Id.otpInitView).Visibility =
|
|
_challengeSecret == null ? ViewStates.Visible : ViewStates.Gone;
|
|
}
|
|
|
|
//use !IsFinishing to make sure we're not starting another activity when we're already finishing (e.g. due to TaskComplete in OnActivityResult)
|
|
//use !performingLoad to make sure we're not already loading the database (after ActivityResult from File-Prepare-Activity; this would cause _loadDbFileTask to exist when we reload later!)
|
|
if (!IsFinishing && !_performingLoad)
|
|
{
|
|
|
|
|
|
// OnResume is run every time the activity comes to the foreground. This code should only run when the activity is started (OnStart), but must
|
|
// be run in OnResume rather than OnStart so that it always occurrs after OnActivityResult (when re-creating a killed activity, OnStart occurs before OnActivityResult)
|
|
if (_starting)
|
|
{
|
|
|
|
_starting = false;
|
|
|
|
//database not yet loaded.
|
|
|
|
//check if pre-loading is enabled but wasn't started yet:
|
|
if (_loadDbFileTask == null &&
|
|
_prefs.GetBoolean(GetString(Resource.String.PreloadDatabaseEnabled_key), true))
|
|
{
|
|
// Create task to kick off file loading while the user enters the password
|
|
_loadDbFileTask = Task.Factory.StartNew(PreloadDbFile);
|
|
_loadDbTaskOffline = App.Kp2a.OfflineMode;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (compositeKeyForImmediateLoad != null)
|
|
{
|
|
//reload the database (without most other stuff performed in PerformLoadDatabase.
|
|
// We're assuming that the db file (and if appropriate also the key file) are still available
|
|
// and there's no need to re-init the file storage. if it is, loading will fail and the user has
|
|
// to retry with typing the full password, but that's intended to avoid showing the password to a
|
|
// a potentially unauthorized user (feature request https://keepass2android.codeplex.com/workitem/274)
|
|
Handler handler = new Handler();
|
|
OnFinish onFinish = new AfterLoad(handler, this, _ioConnection);
|
|
_performingLoad = true;
|
|
LoadDb task = new LoadDb(this, App.Kp2a, _ioConnection, _loadDbFileTask, compositeKeyForImmediateLoad, GetKeyProviderString(),
|
|
onFinish, false, _makeCurrent);
|
|
_loadDbFileTask = null; // prevent accidental re-use
|
|
new ProgressTask(App.Kp2a, this, task).Run();
|
|
compositeKeyForImmediateLoad = null; //don't reuse or keep in memory
|
|
|
|
}
|
|
else
|
|
{
|
|
bool showKeyboard = true;
|
|
|
|
|
|
EditText pwd = (EditText)FindViewById(Resource.Id.password_edit);
|
|
pwd.PostDelayed(() =>
|
|
{
|
|
InputMethodManager keyboard = (InputMethodManager)GetSystemService(InputMethodService);
|
|
if (showKeyboard)
|
|
{
|
|
pwd.RequestFocus();
|
|
keyboard.ShowSoftInput(pwd, 0);
|
|
}
|
|
else
|
|
keyboard.HideSoftInputFromWindow(pwd.WindowToken, HideSoftInputFlags.ImplicitOnly);
|
|
}, 50);
|
|
}
|
|
|
|
_resumeCompleted = true;
|
|
}
|
|
|
|
private void TryGetOtpFromClipboard()
|
|
{
|
|
if (_otpInfo != null)
|
|
{
|
|
if ((int) Build.VERSION.SdkInt >= 26)
|
|
{
|
|
Android.Content.ClipboardManager clipboardManager = (ClipboardManager)GetSystemService(Context.ClipboardService);
|
|
if (clipboardManager?.PrimaryClip?.Description == null || (clipboardManager.PrimaryClip.Description.Timestamp <
|
|
Java.Lang.JavaSystem.CurrentTimeMillis() - 5000))
|
|
return; //data older than 5 seconds
|
|
}
|
|
string clipboardContent = Util.GetClipboard(this);
|
|
if (clipboardContent == null || (_otpInfo.OtpLength != clipboardContent.Length))
|
|
{
|
|
return;
|
|
}
|
|
foreach (char c in clipboardContent)
|
|
{
|
|
if (c < '0' || c > '9')
|
|
return;
|
|
}
|
|
string otp = clipboardContent;
|
|
|
|
EditText lastNonEmptyOtpEdit = null;
|
|
|
|
foreach (int otpId in _otpTextViewIds)
|
|
{
|
|
EditText otpEdit = FindViewById<EditText>(otpId);
|
|
if (otpEdit?.Visibility == ViewStates.Visible)
|
|
{
|
|
if (string.IsNullOrEmpty(otpEdit.Text))
|
|
{
|
|
if ((lastNonEmptyOtpEdit != null) && (lastNonEmptyOtpEdit.Text == otp))
|
|
return; //otp was already set.
|
|
|
|
//otp ok. use it:
|
|
otpEdit.Text = otp;
|
|
break;
|
|
}
|
|
|
|
lastNonEmptyOtpEdit = otpEdit;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
private bool InitFingerprintUnlock()
|
|
{
|
|
var btn = FindViewById<ImageButton>(Resource.Id.fingerprintbtn);
|
|
try
|
|
{
|
|
FingerprintUnlockMode um;
|
|
Enum.TryParse(_prefs.GetString(Database.GetFingerprintModePrefKey(_ioConnection), ""), out um);
|
|
btn.Visibility = (um == FingerprintUnlockMode.FullUnlock) ? ViewStates.Visible : ViewStates.Gone;
|
|
|
|
if (um != FingerprintUnlockMode.FullUnlock)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
BiometricModule fpModule = new BiometricModule(this);
|
|
_biometricDec = new BiometricDecryption(fpModule, Database.GetFingerprintPrefKey(_ioConnection), this,
|
|
Database.GetFingerprintPrefKey(_ioConnection));
|
|
|
|
if (_biometricDec.Init())
|
|
{
|
|
btn.SetImageResource(Resource.Drawable.baseline_fingerprint_24);
|
|
_biometricDec.StartListening(new BiometricAuthCallbackAdapter(this, this));
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
Kp2aLog.Log("biometricDec.Init() failed");
|
|
HandleFingerprintKeyInvalidated();
|
|
return false;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
//exception can happen here if the app was restored from Google Backup (including preferences) but no fingerprint data is there.
|
|
btn.SetImageResource(Resource.Drawable.ic_fingerprint_error);
|
|
Kp2aLog.Log("failed to init fingerprint unlock:" + e);
|
|
string error = GetString(Resource.String.FingerprintInitFailed) + " " +
|
|
GetString(Resource.String.fingerprint_reenable2);
|
|
|
|
btn.Tag = error;
|
|
|
|
App.Kp2a.ShowMessage(this, Resource.String.fingerprint_reenable2, MessageSeverity.Error);
|
|
|
|
_biometricDec = null;
|
|
return false;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
private void HandleFingerprintKeyInvalidated()
|
|
{
|
|
var btn = FindViewById<ImageButton>(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);
|
|
_biometricDec = null;
|
|
|
|
ClearFingerprintUnlockData();
|
|
}
|
|
|
|
private void InitializeOptionCheckboxes() {
|
|
CheckBox cbQuickUnlock = (CheckBox)FindViewById(Resource.Id.enable_quickunlock);
|
|
cbQuickUnlock.Checked = _prefs.GetBoolean(GetString(Resource.String.QuickUnlockDefaultEnabled_key), true);
|
|
|
|
CheckBox cbOfflineMode = (CheckBox)FindViewById(Resource.Id.work_offline);
|
|
cbOfflineMode.CheckedChange += (sender, args) =>
|
|
{
|
|
App.Kp2a.OfflineModePreference = App.Kp2a.OfflineMode = args.IsChecked;
|
|
};
|
|
|
|
}
|
|
|
|
private String LoadKeyProviderStringForIoc(String filename) {
|
|
if ( _rememberKeyfile ) {
|
|
string keyfile = App.Kp2a.FileDbHelper.GetKeyFileForFile(filename);
|
|
if (String.IsNullOrEmpty(keyfile))
|
|
return null; //signal no key file
|
|
|
|
return keyfile;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private void InitializeFilenameView()
|
|
{
|
|
string filenameToShow = _ioConnection.Path;
|
|
try
|
|
{
|
|
filenameToShow = App.Kp2a.GetFileStorage(_ioConnection).GetDisplayName(_ioConnection);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Kp2aLog.LogUnexpectedError(e);
|
|
}
|
|
SetEditText(Resource.Id.filename, filenameToShow);
|
|
|
|
}
|
|
|
|
protected override void OnDestroy()
|
|
{
|
|
UnregisterReceiver(_intentReceiver);
|
|
base.OnDestroy();
|
|
if (_killOnDestroy)
|
|
Process.KillProcess(Process.MyPid());
|
|
}
|
|
|
|
/*
|
|
private void errorMessage(CharSequence text)
|
|
{
|
|
App.Kp2a.ShowMessage(this, text, MessageSeverity.Error);
|
|
}
|
|
*/
|
|
|
|
private void SetEditText(int resId, String str) {
|
|
TextView te = (TextView) FindViewById(resId);
|
|
//assert(te == null);
|
|
|
|
if (te != null) {
|
|
te.Text = str;
|
|
}
|
|
}
|
|
|
|
public override bool OnOptionsItemSelected(IMenuItem item) {
|
|
switch ( item.ItemId ) {
|
|
|
|
case Android.Resource.Id.Home:
|
|
_drawerLayout.OpenDrawer(GravityCompat.Start);
|
|
return true;
|
|
}
|
|
|
|
return base.OnOptionsItemSelected(item);
|
|
}
|
|
|
|
private void GoToFileSelectActivity()
|
|
{
|
|
SetResult((Result) ResultSelectOtherFile);
|
|
Finish();
|
|
}
|
|
|
|
private class AfterLoad : OnFinish {
|
|
readonly PasswordActivity _act;
|
|
private readonly IOConnectionInfo _ioConnection;
|
|
|
|
public AfterLoad(Handler handler, PasswordActivity act, IOConnectionInfo ioConnection):base(act, handler)
|
|
{
|
|
_act = act;
|
|
_ioConnection = ioConnection;
|
|
}
|
|
|
|
|
|
public override void Run()
|
|
{
|
|
if (Success)
|
|
{
|
|
|
|
_act.ClearEnteredPassword();
|
|
_act.BroadcastOpenDatabase();
|
|
_act.InvalidCompositeKeyCount = 0;
|
|
_act.LoadingErrorCount = 0;
|
|
|
|
|
|
GC.Collect(); // Ensure temporary memory used while loading is collected
|
|
}
|
|
|
|
if (Exception != null)
|
|
{
|
|
_act.LoadingErrorCount++;
|
|
}
|
|
|
|
|
|
|
|
if ((Exception != null) && (Exception.Message == KeePassLib.Resources.KLRes.FileCorrupted))
|
|
{
|
|
Message = _act.GetString(Resource.String.CorruptDatabaseHelp);
|
|
}
|
|
|
|
if (Exception is InvalidCompositeKeyException)
|
|
{
|
|
_act.InvalidCompositeKeyCount++;
|
|
if (_act.UsedFingerprintUnlock)
|
|
{
|
|
//disable fingerprint unlock if master password changed
|
|
_act.ClearFingerprintUnlockData();
|
|
_act.InitFingerprintUnlock();
|
|
|
|
Message = _act.GetString(Resource.String.fingerprint_disabled_wrong_masterkey) + " " +
|
|
_act.GetString(Resource.String.fingerprint_reenable2);
|
|
}
|
|
else
|
|
{
|
|
if (_act.InvalidCompositeKeyCount > 1)
|
|
{
|
|
Message = _act.GetString(Resource.String.RepeatedInvalidCompositeKeyHelp);
|
|
if (_act._prefs.GetBoolean(IoUtil.GetIocPrefKey(_ioConnection, "has_local_backup"), false))
|
|
{
|
|
Java.Lang.Object changeDb = _act.GetString(Resource.String.menu_change_db);
|
|
Message += _act.GetString(Resource.String.HintLocalBackupInvalidCompositeKey, new Java.Lang.Object[] {changeDb});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Message = _act.GetString(Resource.String.FirstInvalidCompositeKeyError);
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
else if (_act.LoadingErrorCount > 1)
|
|
{
|
|
if (_act._prefs.GetBoolean(IoUtil.GetIocPrefKey(_ioConnection, "has_local_backup"), false))
|
|
{
|
|
Object changeDb = _act.GetString(Resource.String.menu_change_db);
|
|
Message += _act.GetString(Resource.String.HintLocalBackupOtherError, changeDb);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((Message != null) && (Message.Length > 150)) //show long messages as dialog
|
|
{
|
|
new MaterialAlertDialogBuilder(_act).SetMessage(Message)
|
|
.SetPositiveButton(Android.Resource.String.Ok,
|
|
(sender, args) =>
|
|
{
|
|
((Dialog) sender).Dismiss();
|
|
if (Success)
|
|
{
|
|
_act.LaunchNextActivity();
|
|
}
|
|
})
|
|
.SetCancelable(false)
|
|
.Show();
|
|
|
|
}
|
|
else
|
|
{
|
|
MessageSeverity severity = Success ? MessageSeverity.Info : MessageSeverity.Error;
|
|
App.Kp2a.ShowMessage(_act, Message, severity);
|
|
if (Success)
|
|
{
|
|
_act.LaunchNextActivity();
|
|
}
|
|
|
|
}
|
|
|
|
//re-init fingerprint unlock in case something went wrong with opening the database
|
|
if (!Success)
|
|
_act.InitFingerprintUnlock();
|
|
|
|
|
|
_act._performingLoad = false;
|
|
|
|
}
|
|
}
|
|
|
|
|
|
private void BroadcastOpenDatabase()
|
|
{
|
|
App.Kp2a.BroadcastDatabaseAction(this, Strings.ActionOpenDatabase);
|
|
}
|
|
|
|
private void ClearEnteredPassword()
|
|
{
|
|
SetEditText(Resource.Id.password_edit, "");
|
|
SetEditText(Resource.Id.pass_otpsecret, "");
|
|
foreach (int otpId in _otpTextViewIds)
|
|
{
|
|
SetEditText(otpId, "");
|
|
}
|
|
if (_challengeSecret != null)
|
|
{
|
|
Array.Clear(_challengeSecret, 0, _challengeSecret.Length);
|
|
_challengeSecret = null;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
class SaveOtpAuxFileAndLoadDb : LoadDb
|
|
{
|
|
private readonly PasswordActivity _act;
|
|
|
|
|
|
public SaveOtpAuxFileAndLoadDb(IKp2aApp app, IOConnectionInfo ioc, Task<MemoryStream> databaseData, CompositeKey compositeKey, string keyfileOrProvider, OnFinish finish, PasswordActivity act, bool updateLastUsageTimestamp, bool makeCurrent) : base(act, app, ioc, databaseData, compositeKey, keyfileOrProvider, finish,updateLastUsageTimestamp,makeCurrent)
|
|
{
|
|
_act = act;
|
|
}
|
|
|
|
public override void Run()
|
|
{
|
|
try
|
|
{
|
|
StatusLogger.UpdateMessage(UiStringKey.SavingOtpAuxFile);
|
|
|
|
KeyProviderQueryContext ctx = new KeyProviderQueryContext(_act._ioConnection, false, false);
|
|
|
|
if (!OathHotpKeyProv.CreateAuxFile(_act._otpInfo, ctx, _act._otpAuxIoc))
|
|
ShowError(_act.GetString(Resource.String.ErrorUpdatingOtpAuxFile));
|
|
|
|
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Kp2aLog.LogUnexpectedError(e);
|
|
|
|
ShowError( _act.GetString(Resource.String.ErrorUpdatingOtpAuxFile) + " " + Util.GetErrorMessage(e));
|
|
}
|
|
|
|
|
|
base.Run();
|
|
|
|
if (success)
|
|
{
|
|
App.Kp2a.CurrentDb.OtpAuxFileIoc = _act._otpAuxIoc;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
private void ShowError(string message)
|
|
{
|
|
App.Kp2a.ShowToast(message, MessageSeverity.Error);
|
|
}
|
|
}
|
|
private class PasswordActivityBroadcastReceiver : BroadcastReceiver
|
|
{
|
|
readonly PasswordActivity _activity;
|
|
public PasswordActivityBroadcastReceiver(PasswordActivity activity)
|
|
{
|
|
_activity = activity;
|
|
}
|
|
|
|
public override void OnReceive(Context context, Intent intent)
|
|
{
|
|
switch (intent.Action)
|
|
{
|
|
case Intent.ActionScreenOff:
|
|
_activity.OnScreenLocked();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnScreenLocked()
|
|
{
|
|
if (_biometricDec != null)
|
|
_biometricDec.StopListening();
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|