/* 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