/*
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.Text;
using System.Linq;
using System.Threading;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Widget;
using Android.Preferences;
using Android.Text.Method;
using System.Globalization;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Android.Content.PM;
using Android.Webkit;
using Android.Graphics;
using keepass2android.EntryActivityClasses;
using KeePassLib;
using KeePassLib.Security;
using KeePassLib.Utility;
using Keepass2android.Pluginsdk;
using keepass2android.Io;
using KeePass.DataExchange;
using KeePass.Util.Spr;
using KeePassLib.Interfaces;
using KeePassLib.Serialization;
using PluginTOTP;
using File = Java.IO.File;
using Uri = Android.Net.Uri;
using keepass2android.fileselect;
using KeeTrayTOTP.Libraries;
using Boolean = Java.Lang.Boolean;
using Android.Util;
using AndroidX.Core.Content;
using Google.Android.Material.Dialog;
using keepass2android;
namespace keepass2android
{
public class ExportBinaryProcessManager : FileSaveProcessManager
{
private readonly string _binaryToSave;
public ExportBinaryProcessManager(int requestCode, Activity activity, string key) : base(requestCode, activity)
{
_binaryToSave = key;
}
public ExportBinaryProcessManager(int requestCode, EntryActivity activity, Bundle savedInstanceState) : base(requestCode, activity)
{
_binaryToSave = savedInstanceState.GetString("BinaryToSave", null);
}
protected override void SaveFile(IOConnectionInfo ioc)
{
var task = new EntryActivity.WriteBinaryTask(_activity, App.Kp2a, new ActionOnFinish(_activity, (success, message, activity) =>
{
if (!success)
App.Kp2a.ShowMessage(activity, message, MessageSeverity.Error);
}
), ((EntryActivity)_activity).Entry.Binaries.Get(_binaryToSave), ioc);
ProgressTask pt = new ProgressTask(App.Kp2a, _activity, task);
pt.Run();
}
public override void OnSaveInstanceState(Bundle outState)
{
outState.PutString("BinaryToSave", _binaryToSave);
}
}
[Activity (Label = "@string/app_name", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden,
Theme = "@style/Kp2aTheme_ActionBar")]
public class EntryActivity : LockCloseActivity
{
public const String KeyEntry = "entry";
public const String KeyRefreshPos = "refresh_pos";
public const String KeyEntryHistoryIndex = "entry_history_index";
public const String KeyActivateKeyboard = "activate_keyboard";
public const String KeyGroupFullPath = "groupfullpath_key";
public const int requestCodeBinaryFilename = 42376;
public const int requestCodeSelFileStorageForWriteAttachment = 42377;
protected override View? SnackbarAnchorView => FindViewById(Resource.Id.main_content);
public static void Launch(Activity act, PwEntry pw, int pos, AppTask appTask, ActivityFlags? flags = null, int historyIndex=-1)
{
Intent i = new Intent(act, typeof(EntryActivity));
var db = App.Kp2a.FindDatabaseForElement(pw);
i.PutExtra(KeyEntry, new ElementAndDatabaseId(db, pw).FullId);
i.PutExtra(KeyRefreshPos, pos);
i.PutExtra(KeyEntryHistoryIndex, historyIndex);
if (App.Kp2a.CurrentDb != db)
{
App.Kp2a.CurrentDb = db;
}
if (flags != null)
i.SetFlags((ActivityFlags) flags);
appTask.ToIntent(i);
if (flags != null && (((ActivityFlags) flags) | ActivityFlags.ForwardResult) == ActivityFlags.ForwardResult)
act.StartActivity(i);
else
act.StartActivityForResult(i, 0);
}
public EntryActivity (IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
public EntryActivity()
{
_activityDesign = new ActivityDesign(this);
}
//this is the entry we display. Note that it might be an element from a History list in case _historyIndex >= 0
public PwEntry Entry;
//if _historyIndex >=0, _historyParentEntry stores the PwEntry which contains the history entry "Entry"
private PwEntry _historyParentEntry;
private PasswordFont _passwordFont = new PasswordFont();
internal Dictionary _showPassword = new Dictionary();
private int _pos;
private AppTask _appTask;
private AppTask AppTask
{
get { return _appTask; }
set
{
_appTask = value;
Kp2aLog.LogTask(value, MyDebugName);
}
}
struct ProtectedTextviewGroup
{
public TextView ProtectedField;
public TextView VisibleProtectedField;
}
private List _protectedTextViews;
private IMenu _menu;
private readonly Dictionary> _popupMenuItems =
new Dictionary>();
private readonly Dictionary _stringViews = new Dictionary();
private readonly List _pendingMenuOptions = new List();
//make sure _timer doesn't go out of scope:
private Timer _timer;
private PluginActionReceiver _pluginActionReceiver;
private PluginFieldReceiver _pluginFieldReceiver;
private ActivityDesign _activityDesign;
protected void SetEntryView()
{
SetContentView(Resource.Layout.entry_view);
}
protected void SetupEditButtons() {
View edit = FindViewById(Resource.Id.entry_edit);
if (App.Kp2a.CurrentDb.CanWrite && _historyIndex < 0)
{
edit.Visibility = ViewStates.Visible;
edit.Click += (sender, e) =>
{
EntryEditActivity.Launch(this, Entry, AppTask);
};
}
else
{
edit.Visibility = ViewStates.Gone;
}
}
private class PluginActionReceiver : BroadcastReceiver
{
private readonly EntryActivity _activity;
public PluginActionReceiver(EntryActivity activity)
{
_activity = activity;
}
public override void OnReceive(Context context, Intent intent)
{
var pluginPackage = intent.GetStringExtra(Strings.ExtraSender);
if (new PluginDatabase(context).IsValidAccessToken(pluginPackage,
intent.GetStringExtra(Strings.ExtraAccessToken),
Strings.ScopeCurrentEntry))
{
if (intent.GetStringExtra(Strings.ExtraEntryId) != _activity.Entry.Uuid.ToHexString())
{
Kp2aLog.Log("received action for wrong entry " + intent.GetStringExtra(Strings.ExtraEntryId));
return;
}
_activity.AddPluginAction(pluginPackage,
intent.GetStringExtra(Strings.ExtraFieldId),
intent.GetStringExtra(Strings.ExtraActionId),
intent.GetStringExtra(Strings.ExtraActionDisplayText),
intent.GetIntExtra(Strings.ExtraActionIconResId, -1),
intent.GetBundleExtra(Strings.ExtraActionData));
}
else
{
Kp2aLog.Log("received invalid request. Plugin not authorized.");
}
}
}
private class PluginFieldReceiver : BroadcastReceiver
{
private readonly EntryActivity _activity;
public PluginFieldReceiver(EntryActivity activity)
{
_activity = activity;
}
public override void OnReceive(Context context, Intent intent)
{
if (intent.GetStringExtra(Strings.ExtraEntryId) != _activity.Entry.Uuid.ToHexString())
{
Kp2aLog.Log("received field for wrong entry " + intent.GetStringExtra(Strings.ExtraEntryId));
return;
}
if (!new PluginDatabase(context).IsValidAccessToken(intent.GetStringExtra(Strings.ExtraSender),
intent.GetStringExtra(Strings.ExtraAccessToken),
Strings.ScopeCurrentEntry))
{
Kp2aLog.Log("received field with invalid access token from " + intent.GetStringExtra(Strings.ExtraSender));
return;
}
string key = intent.GetStringExtra(Strings.ExtraFieldId);
string value = intent.GetStringExtra(Strings.ExtraFieldValue);
bool isProtected = intent.GetBooleanExtra(Strings.ExtraFieldProtected, false);
_activity.SetPluginField(key, value, isProtected);
}
}
private void SetPluginField(string key, string value, bool isProtected)
{
//update or add the string view:
IStringView existingField;
if (_stringViews.TryGetValue(key, out existingField))
{
existingField.Text = value;
}
else
{
ViewGroup extraGroup = (ViewGroup) FindViewById(Resource.Id.extra_strings);
var view = CreateExtraSection(key, value, isProtected);
extraGroup.AddView(view.View);
}
SetPasswordStyle();
//update the Entry output in the App database and notify the CopyToClipboard service
if (App.Kp2a.LastOpenedEntry != null)
{
App.Kp2a.LastOpenedEntry.OutputStrings.Set(key, new ProtectedString(isProtected, value));
Intent updateKeyboardIntent = new Intent(this, typeof(CopyToClipboardService));
updateKeyboardIntent.SetAction(Intents.UpdateKeyboard);
updateKeyboardIntent.PutExtra(KeyEntry, new ElementAndDatabaseId(App.Kp2a.CurrentDb, Entry).FullId);
StartService(updateKeyboardIntent);
//notify plugins
NotifyPluginsOnModification(Strings.PrefixString + key);
}
}
private void AddPluginAction(string pluginPackage, string fieldId, string popupItemId, string displayText, int iconId, Bundle bundleExtra)
{
if (fieldId != null)
{
try
{
if (!_popupMenuItems.ContainsKey(fieldId))
{
Kp2aLog.Log("Did not find field with key " + fieldId);
return;
}
//create a new popup item for the plugin action:
var newPopup = new PluginPopupMenuItem(this, pluginPackage, fieldId, popupItemId, displayText, iconId, bundleExtra);
//see if we already have a popup item for this field with the same item id
var popupsForField = _popupMenuItems[fieldId];
var popupItemPos = popupsForField.FindIndex(0,
item =>
(item is PluginPopupMenuItem) &&
((PluginPopupMenuItem)item).PopupItemId == popupItemId);
//replace existing or add
if (popupItemPos >= 0)
{
popupsForField[popupItemPos] = newPopup;
}
else
{
popupsForField.Add(newPopup);
}
}
catch (Exception e)
{
Kp2aLog.LogUnexpectedError(e);
}
}
else
{
//we need to add an option to the menu.
//As it is not sure that OnCreateOptionsMenu was called yet, we cannot access _menu without a check:
Intent i = new Intent(Strings.ActionEntryActionSelected);
i.SetPackage(pluginPackage);
i.PutExtra(Strings.ExtraActionData, bundleExtra);
i.PutExtra(Strings.ExtraSender, PackageName);
PluginHost.AddEntryToIntent(i, App.Kp2a.LastOpenedEntry);
var menuOption = new PluginMenuOption()
{
DisplayText = displayText,
Icon = PackageManager.GetResourcesForApplication(pluginPackage).GetDrawable(iconId),
Intent = i
};
if (_menu != null)
{
AddMenuOption(menuOption);
}
else
{
lock (_pendingMenuOptions)
{
_pendingMenuOptions.Add(menuOption);
}
}
}
}
private void AddMenuOption(PluginMenuOption menuOption)
{
var menuItem = _menu.Add(menuOption.DisplayText);
menuItem.SetIcon(menuOption.Icon);
menuItem.SetIntent(menuOption.Intent);
}
protected override void OnCreate(Bundle savedInstanceState)
{
if (savedInstanceState != null)
{
_exportBinaryProcessManager =
new ExportBinaryProcessManager(requestCodeSelFileStorageForWriteAttachment, this, savedInstanceState);
}
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
long usageCount = prefs.GetLong(GetString(Resource.String.UsageCount_key), 0);
ISharedPreferencesEditor edit = prefs.Edit();
edit.PutLong(GetString(Resource.String.UsageCount_key), usageCount + 1);
edit.Commit();
_showPasswordDefault =
!prefs.GetBoolean(GetString(Resource.String.maskpass_key), Resources.GetBoolean(Resource.Boolean.maskpass_default));
_showTotpDefault =
!prefs.GetBoolean(GetString(Resource.String.masktotp_key), Resources.GetBoolean(Resource.Boolean.masktotp_default));
RequestWindowFeature(WindowFeatures.IndeterminateProgress);
_activityDesign.ApplyTheme();
base.OnCreate(savedInstanceState);
SetEntryView();
Database db = App.Kp2a.CurrentDb;
// Likely the app has been killed exit the activity
if (db == null || (App.Kp2a.QuickLocked))
{
Finish();
return;
}
SetResult(KeePass.ExitNormal);
Intent i = Intent;
ElementAndDatabaseId dbAndElementId = new ElementAndDatabaseId(i.GetStringExtra(KeyEntry));
PwUuid uuid = new PwUuid(MemUtil.HexStringToByteArray(dbAndElementId.ElementIdString));
_pos = i.GetIntExtra(KeyRefreshPos, -1);
_historyIndex = i.GetIntExtra(KeyEntryHistoryIndex, -1);
AppTask = AppTask.GetTaskInOnCreate(savedInstanceState, Intent);
Entry = db.EntriesById[uuid];
if (_historyIndex >= 0 && _historyIndex < Entry.History.UCount)
{
_historyParentEntry = Entry;
Entry = Entry.History.Skip(_historyIndex).First();
FindViewById