diff --git a/src/Kp2aBusinessLogic/SearchDbHelper.cs b/src/Kp2aBusinessLogic/SearchDbHelper.cs index 3b3207b27..a51e0b7a1 100644 --- a/src/Kp2aBusinessLogic/SearchDbHelper.cs +++ b/src/Kp2aBusinessLogic/SearchDbHelper.cs @@ -91,7 +91,29 @@ public PwGroup SearchForExactUrl (Database database, string url) } - private static String ExtractHost(String url) + public PwGroup SearchForUuid(Database database, string uuid) + { + SearchParameters sp = SearchParameters.None; + sp.SearchInUuids = true; + sp.SearchString = uuid; + + if (sp.RegularExpression) // Validate regular expression + { + new Regex(sp.SearchString); + } + + string strGroupName = _app.GetResourceString(UiStringKey.search_results); + PwGroup pgResults = new PwGroup(true, true, strGroupName, PwIcon.EMailSearch) { IsVirtual = true }; + + PwObjectList listResults = pgResults.Entries; + + database.Root.SearchEntries(sp, listResults, new NullStatusLogger()); + + return pgResults; + + } + + private static String ExtractHost(String url) { return UrlUtil.GetHost(url.Trim()); } diff --git a/src/Kp2aBusinessLogic/database/Database.cs b/src/Kp2aBusinessLogic/database/Database.cs index c74c043b6..a94c4ac4b 100644 --- a/src/Kp2aBusinessLogic/database/Database.cs +++ b/src/Kp2aBusinessLogic/database/Database.cs @@ -174,10 +174,17 @@ public PwGroup SearchForExactUrl(String url) { PwGroup group = SearchHelper.SearchForExactUrl(this, url); return group; - - } - public PwGroup SearchForHost(String url, bool allowSubdomains) { + } + public PwGroup SearchForUuid(String uuid) + { + PwGroup group = SearchHelper.SearchForUuid(this, uuid); + + return group; + + } + + public PwGroup SearchForHost(String url, bool allowSubdomains) { PwGroup group = SearchHelper.SearchForHost(this, url, allowSubdomains); return group; diff --git a/src/keepass2android/EntryActivity.cs b/src/keepass2android/EntryActivity.cs index e96f11ba0..91545a379 100644 --- a/src/keepass2android/EntryActivity.cs +++ b/src/keepass2android/EntryActivity.cs @@ -32,6 +32,7 @@ You should have received a copy of the GNU General Public License using System.Globalization; using System.IO; using System.Net; +using System.Threading.Tasks; using Android.Content.PM; using Android.Webkit; using Android.Graphics; @@ -49,7 +50,9 @@ You should have received a copy of the GNU General Public License using File = Java.IO.File; using Uri = Android.Net.Uri; using keepass2android.fileselect; +using KeeTrayTOTP.Libraries; using Boolean = Java.Lang.Boolean; +using Android.Util; namespace keepass2android { @@ -286,6 +289,8 @@ private void SetPluginField(string key, string value, bool isProtected) extraGroup.AddView(view.View); } + SetPasswordStyle(); + //update the Entry output in the App database and notify the CopyToClipboard service if (App.Kp2a.LastOpenedEntry != null) @@ -488,10 +493,11 @@ protected override void OnCreate(Bundle savedInstanceState) _pluginFieldReceiver = new PluginFieldReceiver(this); RegisterReceiver(_pluginFieldReceiver, new IntentFilter(Strings.ActionSetEntryField)); - new Thread(NotifyPluginsOnOpen).Start(); + var notifyPluginsOnOpenThread = new Thread(NotifyPluginsOnOpen); + notifyPluginsOnOpenThread.Start(); //the rest of the things to do depends on the current app task: - AppTask.CompleteOnCreateEntryActivity(this); + AppTask.CompleteOnCreateEntryActivity(this, notifyPluginsOnOpenThread); } private void RemoveFromHistory() @@ -664,7 +670,7 @@ private void PopulateExtraStrings() EditModeBase editMode = new DefaultEdit(); if (KpEntryTemplatedEdit.IsTemplated(App.Kp2a.CurrentDb, this.Entry)) editMode = new KpEntryTemplatedEdit(App.Kp2a.CurrentDb, this.Entry); - foreach (var key in editMode.SortExtraFieldKeys(Entry.Strings.GetKeys().Where(key=> !PwDefs.IsStandardField(key)))) + foreach (var key in editMode.SortExtraFieldKeys(Entry.Strings.GetKeys().Where(key=> !PwDefs.IsStandardField(key) && key != Kp2aTotp.TotpKey))) { if (editMode.IsVisible(key)) { @@ -840,7 +846,7 @@ private void RegisterProtectedTextView(string fieldKey, TextView protectedTextVi { if (!_showPassword.ContainsKey(protectedTextView)) { - _showPassword[protectedTextView] = fieldKey == UpdateTotpTimerTask.TotpKey ? _showTotpDefault : _showPasswordDefault; + _showPassword[protectedTextView] = fieldKey == Kp2aTotp.TotpKey ? _showTotpDefault : _showPasswordDefault; } var protectedTextviewGroup = new ProtectedTextviewGroup { ProtectedField = protectedTextView, VisibleProtectedField = visibleTextView}; _protectedTextViews.Add(protectedTextviewGroup); @@ -946,11 +952,13 @@ protected void FillData() PopulateStandardText(Resource.Id.entry_user_name, Resource.Id.entryfield_container_username, PwDefs.UserNameField); PopulateStandardText(Resource.Id.entry_url, Resource.Id.entryfield_container_url, PwDefs.UrlField); - PopulateStandardText(new List { Resource.Id.entry_password, Resource.Id.entry_password_visible}, Resource.Id.entryfield_container_password, PwDefs.PasswordField); + PopulateStandardText(new List { Resource.Id.entry_totp, Resource.Id.entry_totp_visible }, Resource.Id.entryfield_container_totp, Kp2aTotp.TotpKey); + PopulateStandardText(new List { Resource.Id.entry_password, Resource.Id.entry_password_visible}, Resource.Id.entryfield_container_password, PwDefs.PasswordField); RegisterProtectedTextView(PwDefs.PasswordField, FindViewById(Resource.Id.entry_password), FindViewById(Resource.Id.entry_password_visible)); + RegisterProtectedTextView(Kp2aTotp.TotpKey, FindViewById(Resource.Id.entry_totp), FindViewById(Resource.Id.entry_totp_visible)); - RegisterTextPopup(FindViewById (Resource.Id.groupname_container), + RegisterTextPopup(FindViewById (Resource.Id.groupname_container), FindViewById (Resource.Id.entry_group_name), KeyGroupFullPath); RegisterTextPopup(FindViewById(Resource.Id.username_container), @@ -961,9 +969,11 @@ protected void FillData() .Add(new GotoUrlMenuItem(this, PwDefs.UrlField)); RegisterTextPopup(FindViewById(Resource.Id.password_container), FindViewById(Resource.Id.password_vdots), PwDefs.PasswordField); + RegisterTextPopup(FindViewById(Resource.Id.totp_container), + FindViewById(Resource.Id.totp_vdots), Kp2aTotp.TotpKey); - PopulateText(Resource.Id.entry_created, Resource.Id.entryfield_container_created, getDateTime(Entry.CreationTime)); + PopulateText(Resource.Id.entry_created, Resource.Id.entryfield_container_created, getDateTime(Entry.CreationTime)); PopulateText(Resource.Id.entry_modified, Resource.Id.entryfield_container_modified, getDateTime(Entry.LastModificationTime)); if (Entry.Expires) @@ -990,6 +1000,40 @@ protected void FillData() SetPasswordStyle(); } + + private async Task UpdateTotpCountdown() + { + if (App.Kp2a.LastOpenedEntry == null) + return; + var totpData = new Kp2aTotp().TryGetTotpData(App.Kp2a.LastOpenedEntry); + + if (totpData == null || !totpData.IsTotpEntry) + return; + + var totpProvider = new TOTPProvider(totpData); + + var progressBar = FindViewById(Resource.Id.TotpCountdownProgressBar); + + int lastSecondsLeft = -1; + while (!isPaused && progressBar != null) + { + + int secondsLeft = totpProvider.Timer; + + if (secondsLeft != lastSecondsLeft) + { + lastSecondsLeft = secondsLeft; + // Update the progress bar on the UI thread + RunOnUiThread(() => + { + progressBar.Progress = secondsLeft; + progressBar.Max = totpProvider.Duration; + }); + } + + await Task.Delay(1000); + } + } private void PopulatePreviousVersions() { @@ -1042,7 +1086,7 @@ private void NotifyPluginsOnClose() } private List RegisterTextPopup(View container, View anchor, string fieldKey) { - return RegisterTextPopup(container, anchor, fieldKey, Entry.Strings.GetSafe(fieldKey).IsProtected); + return RegisterTextPopup(container, anchor, fieldKey, Entry.Strings.GetSafe(fieldKey).IsProtected || fieldKey == Kp2aTotp.TotpKey); } private List RegisterTextPopup(View container, View anchor, string fieldKey, bool isProtected) @@ -1055,7 +1099,12 @@ private List RegisterTextPopup(View container, View anchor, stri popupItems.Add(new CopyToClipboardPopupMenuIcon(this, _stringViews[fieldKey], isProtected)); if (isProtected) { - var valueView = container.FindViewById(fieldKey == PwDefs.PasswordField ? Resource.Id.entry_password : Resource.Id.entry_extra); + var valueView = container.FindViewById(fieldKey switch + { + PwDefs.PasswordField => Resource.Id.entry_password, + Kp2aTotp.TotpKey => Resource.Id.entry_totp, + _ => Resource.Id.entry_extra + }); popupItems.Add(new ToggleVisibilityPopupMenuItem(this, valueView)); } @@ -1282,11 +1331,16 @@ public override bool OnPrepareOptionsMenu(IMenu menu) return base.OnPrepareOptionsMenu(menu); } - + bool isPaused = false; - + protected override void OnPause() + { + base.OnPause(); + isPaused = true; + } - private void UpdateTogglePasswordMenu() + + private void UpdateTogglePasswordMenu() { IMenuItem togglePassword = _menu.FindItem(Resource.Id.menu_toggle_pass); if (_showPassword.Values.All(x => x)) @@ -1323,7 +1377,9 @@ protected override void OnResume() ClearCache(); base.OnResume(); _activityDesign.ReapplyTheme(); - } + isPaused = false; + Task.Run(UpdateTotpCountdown); + } public void ClearCache() { diff --git a/src/keepass2android/QueryCredentialsActivity.cs b/src/keepass2android/QueryCredentialsActivity.cs index 1a80b816a..dcf4b6a5f 100644 --- a/src/keepass2android/QueryCredentialsActivity.cs +++ b/src/keepass2android/QueryCredentialsActivity.cs @@ -140,7 +140,7 @@ private void StartQuery() //will return the results later Intent i = new Intent(this, typeof (SelectCurrentDbActivity)); //don't show user notifications when an entry is opened. - var task = new SearchUrlTask() {UrlToSearchFor = _requestedUrl, ShowUserNotifications = ShowUserNotificationsMode.WhenTotp}; + var task = new SearchUrlTask() {UrlToSearchFor = _requestedUrl, ShowUserNotifications = ActivationCondition.WhenTotp, ActivateKeyboard = ActivationCondition.Never }; task.ToIntent(i); StartActivityForResult(i, RequestCodeQuery); _startedQuery = true; diff --git a/src/keepass2android/Resources/drawable-mdpi/ic_entry_totp.png b/src/keepass2android/Resources/drawable-mdpi/ic_entry_totp.png new file mode 100644 index 000000000..12be504c1 Binary files /dev/null and b/src/keepass2android/Resources/drawable-mdpi/ic_entry_totp.png differ diff --git a/src/keepass2android/Resources/drawable-xhdpi/ic_entry_totp.png b/src/keepass2android/Resources/drawable-xhdpi/ic_entry_totp.png new file mode 100644 index 000000000..1e378374c Binary files /dev/null and b/src/keepass2android/Resources/drawable-xhdpi/ic_entry_totp.png differ diff --git a/src/keepass2android/Resources/layout/entry_view_contents.xml b/src/keepass2android/Resources/layout/entry_view_contents.xml index 6f99f9ea7..99444cee4 100644 --- a/src/keepass2android/Resources/layout/entry_view_contents.xml +++ b/src/keepass2android/Resources/layout/entry_view_contents.xml @@ -184,6 +184,68 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/keepass2android/Resources/values/config.xml b/src/keepass2android/Resources/values/config.xml index c9d5eeb84..f3344aba3 100644 --- a/src/keepass2android/Resources/values/config.xml +++ b/src/keepass2android/Resources/values/config.xml @@ -47,6 +47,7 @@ https://openintents.googlecode.com/files/FileManager-2.0.2.apk KP2A Search KP2A Choose autofill dataset + AutoFillTotp_prefs_screen_key diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index 2be1270f5..201b3987a 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -402,6 +402,16 @@ Show separate notifications for copying username and password to clipboard and activating the keyboard. AutoFill Accessibility-Service AutoFill Service + When autofilling an entry with TOTP, show the entry notification with a Copy TOTP button + Show entry notification + Autofill for TOTP entries + Copy TOTP to clipboard + When autofilling an entry with TOTP, copy the TOTP to the clipboard + When autofilling an entry with TOTP, activate the built-in keyboard. The keyboard has a TOTP button. + Activate built-in keyboard + + Copied TOTP to clipboard + KP2A keyboard notification Make full entry accessible through the KP2A keyboard (recommended). Switch keyboard @@ -589,6 +599,7 @@ Please use the KeeChallenge plugin in KeePass 2.x (PC) to configure your database for use with challenge-response! Error updating OTP auxiliary file! TOTP Seed field name + TOTP If you are using the Keepass 2 plugin "TrayTotp" with non-default settings, enter the field name for the seed field here according to the settings on the PC. TOTP Settings field name Enter the field name of the settings field for TrayTotp here. diff --git a/src/keepass2android/Resources/xml/preferences.xml b/src/keepass2android/Resources/xml/preferences.xml index 4e6542fdd..71daf0a28 100644 --- a/src/keepass2android/Resources/xml/preferences.xml +++ b/src/keepass2android/Resources/xml/preferences.xml @@ -461,7 +461,8 @@ android:defaultValue="false" android:title="@string/LogAutofillView_title" android:key="@string/LogAutofillView_key" /> - + + + + + + + + + + + diff --git a/src/keepass2android/SelectCurrentDbActivity.cs b/src/keepass2android/SelectCurrentDbActivity.cs index f21af1b3c..786672a0a 100644 --- a/src/keepass2android/SelectCurrentDbActivity.cs +++ b/src/keepass2android/SelectCurrentDbActivity.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; @@ -306,7 +307,27 @@ protected override void OnCreate(Bundle savedInstanceState) } else if (Intent.Action == Intent.ActionSend) { - AppTask = new SearchUrlTask { UrlToSearchFor = Intent.GetStringExtra(Intent.ExtraText) }; + ActivationCondition activationCondition = ActivationCondition.Never; + ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this); + if (prefs.GetBoolean("kp2a_switch_rooted", false)) + { + activationCondition = ActivationCondition.Always; + } + else + { + //if the app is about to be closed again (e.g. after searching for a URL and returning to the browser: + // automatically bring up the Keyboard selection dialog + if (prefs.GetBoolean(this.GetString(Resource.String.OpenKp2aKeyboardAutomatically_key), this.Resources.GetBoolean(Resource.Boolean.OpenKp2aKeyboardAutomatically_default))) + { + activationCondition = ActivationCondition.Always; + } + } + + AppTask = new SearchUrlTask() + { + UrlToSearchFor = Intent.GetStringExtra(Intent.ExtraText), + ActivateKeyboard = activationCondition + }; } } diff --git a/src/keepass2android/ShareUrlResults.cs b/src/keepass2android/ShareUrlResults.cs index 5eee4b10b..184e4f805 100644 --- a/src/keepass2android/ShareUrlResults.cs +++ b/src/keepass2android/ShareUrlResults.cs @@ -63,6 +63,12 @@ public static void Launch(Activity act, SearchUrlTask task, ActivityLaunchMode l launchMode.Launch(act, i); } + public static void Launch(Activity act, OpenSpecificEntryTask task, ActivityLaunchMode launchMode) + { + Intent i = new Intent(act, typeof(ShareUrlResults)); + task.ToIntent(i); + launchMode.Launch(act, i); + } public override bool IsSearchResult { @@ -76,21 +82,15 @@ protected override void OnCreate(Bundle savedInstanceState) //if user presses back to leave this activity: SetResult(Result.Canceled); - UpdateBottomBarElementVisibility(Resource.Id.select_other_entry, true); UpdateBottomBarElementVisibility(Resource.Id.add_url_entry, true); - if (App.Kp2a.DatabaseIsUnlocked) { - var searchUrlTask = ((SearchUrlTask)AppTask); - String searchUrl = searchUrlTask.UrlToSearchFor; - Query(searchUrl, searchUrlTask.AutoReturnFromQuery); + Query(); } // else: LockCloseListActivity.OnResume will trigger a broadcast (LockDatabase) which will cause the activity to be finished. - - - + } protected override void OnSaveInstanceState(Bundle outState) @@ -99,12 +99,25 @@ protected override void OnSaveInstanceState(Bundle outState) AppTask.ToBundle(outState); } - private void Query(string url, bool autoReturnFromQuery) + private void Query() { - + bool canAutoReturnFromQuery = true; + bool shouldAutoReturnFromQuery = true; try { - Group = GetSearchResultsForUrl(url); + if (AppTask is SearchUrlTask searchUrlTask) + { + String searchUrl = searchUrlTask.UrlToSearchFor; + canAutoReturnFromQuery = searchUrlTask.AutoReturnFromQuery; + shouldAutoReturnFromQuery = PreferenceManager.GetDefaultSharedPreferences(this) + .GetBoolean(GetString(Resource.String.AutoReturnFromQuery_key), true); + Group = GetSearchResultsForUrl(searchUrl); + } + else if (AppTask is OpenSpecificEntryTask openEntryTask) + { + Group = GetSearchResultsForUuid(openEntryTask.EntryUuid); + } + } catch (Exception e) { Toast.MakeText(this, e.Message, ToastLength.Long).Show(); @@ -114,7 +127,7 @@ private void Query(string url, bool autoReturnFromQuery) } //if there is exactly one match: open the entry - if ((Group.Entries.Count() == 1) && autoReturnFromQuery && PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean(GetString(Resource.String.AutoReturnFromQuery_key),true)) + if ((Group.Entries.Count() == 1) && canAutoReturnFromQuery && shouldAutoReturnFromQuery) { LaunchActivityForEntry(Group.Entries.Single(),0); return; @@ -131,32 +144,57 @@ private void Query(string url, bool autoReturnFromQuery) FragmentManager.FindFragmentById(Resource.Id.list_fragment).ListAdapter = new PwGroupListAdapter(this, Group); View selectOtherEntry = FindViewById (Resource.Id.select_other_entry); + View createUrlEntry = FindViewById(Resource.Id.add_url_entry); - var newTask = new SearchUrlTask() {AutoReturnFromQuery = false, UrlToSearchFor = url}; - if (AppTask is SelectEntryTask currentSelectTask) - newTask.ShowUserNotifications = currentSelectTask.ShowUserNotifications; - - selectOtherEntry.Click += (sender, e) => { - GroupActivity.Launch (this, newTask, new ActivityLaunchModeRequestCode(0)); + if (AppTask is OpenSpecificEntryTask) + { + selectOtherEntry.Visibility = ViewStates.Gone; + createUrlEntry.Visibility = ViewStates.Gone; + } + else + { + var searchUrlTask = AppTask as SearchUrlTask; + String searchUrl = searchUrlTask.UrlToSearchFor; + selectOtherEntry.Visibility = ViewStates.Visible; + + SearchUrlTask newTask; + if (AppTask is SelectEntryTask currentSelectTask) + { + newTask = new SearchUrlTask() { AutoReturnFromQuery = false, UrlToSearchFor = searchUrl, ActivateKeyboard = currentSelectTask.ActivateKeyboard }; + newTask.ShowUserNotifications = currentSelectTask.ShowUserNotifications; + newTask.ActivateKeyboard = currentSelectTask.ActivateKeyboard; + newTask.CopyTotpToClipboard = currentSelectTask.CopyTotpToClipboard; + } + else + newTask = new SearchUrlTask() { AutoReturnFromQuery = false, UrlToSearchFor = searchUrl, ActivateKeyboard = ActivationCondition.Never }; + + + selectOtherEntry.Click += (sender, e) => { + GroupActivity.Launch(this, newTask, new ActivityLaunchModeRequestCode(0)); + + }; + + + + + if (App.Kp2a.OpenDatabases.Any(db => db.CanWrite)) + { + createUrlEntry.Visibility = ViewStates.Visible; + createUrlEntry.Click += (sender, e) => + { + GroupActivity.Launch(this, new CreateEntryThenCloseTask { Url = searchUrl, ShowUserNotifications = (AppTask as SelectEntryTask)?.ShowUserNotifications ?? ActivationCondition.Always }, new ActivityLaunchModeRequestCode(0)); + Toast.MakeText(this, GetString(Resource.String.select_group_then_add, new Java.Lang.Object[] { GetString(Resource.String.add_entry) }), ToastLength.Long).Show(); + }; + } + else + { + createUrlEntry.Visibility = ViewStates.Gone; + } + + } - }; - - View createUrlEntry = FindViewById (Resource.Id.add_url_entry); - if (App.Kp2a.OpenDatabases.Any(db => db.CanWrite)) - { - createUrlEntry.Visibility = ViewStates.Visible; - createUrlEntry.Click += (sender, e) => - { - GroupActivity.Launch(this, new CreateEntryThenCloseTask { Url = url, ShowUserNotifications = (AppTask as SelectEntryTask)?.ShowUserNotifications ?? ShowUserNotificationsMode.Always }, new ActivityLaunchModeRequestCode(0)); - Toast.MakeText(this, GetString(Resource.String.select_group_then_add, new Java.Lang.Object[] { GetString(Resource.String.add_entry) }), ToastLength.Long).Show(); - }; - } - else - { - createUrlEntry.Visibility = ViewStates.Gone; - } Util.MoveBottomBarButtons(Resource.Id.select_other_entry, Resource.Id.add_url_entry, Resource.Id.bottom_bar, this); } @@ -201,6 +239,31 @@ public static PwGroup GetSearchResultsForUrl(string url) return resultsGroup; } + + public static PwGroup GetSearchResultsForUuid(string uuid) + { + PwGroup resultsGroup = null; + foreach (var db in App.Kp2a.OpenDatabases) + { + + var resultsForThisDb = db.SearchForUuid(uuid); + + if (resultsGroup == null) + { + resultsGroup = resultsForThisDb; + } + else + { + foreach (var entry in resultsForThisDb.Entries) + { + resultsGroup.AddEntry(entry, false, false); + } + } + } + + return resultsGroup; + } + public override bool OnSearchRequested() { Intent i = new Intent(this, typeof(SearchActivity)); diff --git a/src/keepass2android/Totp/KeeOtpPluginAdapter.cs b/src/keepass2android/Totp/KeeOtpPluginAdapter.cs index f91083434..49d4fd62a 100644 --- a/src/keepass2android/Totp/KeeOtpPluginAdapter.cs +++ b/src/keepass2android/Totp/KeeOtpPluginAdapter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using Android.Content; +using keepass2android; using KeePassLib.Collections; namespace PluginTOTP @@ -39,23 +40,34 @@ public TotpData GetData() { TotpData res = new TotpData(); string data; - if (!_entryFields.TryGetValue("otp", out data)) + var otpKey = "otp"; + if (!_entryFields.TryGetValue(otpKey, out data)) { return res; } NameValueCollection parameters = ParseQueryString(data); + res.InternalFields.Add(otpKey); if (parameters[KeyParameter] == null) { return res; } - res.TotpSeed = parameters[KeyParameter]; - - res.Duration = GetIntOrDefault(parameters, StepParameter, 30).ToString(); - res.Length = GetIntOrDefault(parameters, SizeParameter, 6).ToString(); + try + { + res.TotpSeed = parameters[KeyParameter]; + - res.IsTotpEntry = true; + res.Duration = GetIntOrDefault(parameters, StepParameter, 30).ToString(); + res.Length = GetIntOrDefault(parameters, SizeParameter, 6).ToString(); + + res.IsTotpEntry = true; + } + catch (Exception e) + { + Kp2aLog.Log("Cannot parse seed"); + } + return res; } diff --git a/src/keepass2android/Totp/KeeWebOtpPluginAdapter.cs b/src/keepass2android/Totp/KeeWebOtpPluginAdapter.cs index b83d5edaa..d696cc68d 100644 --- a/src/keepass2android/Totp/KeeWebOtpPluginAdapter.cs +++ b/src/keepass2android/Totp/KeeWebOtpPluginAdapter.cs @@ -16,6 +16,7 @@ public TotpData GetTotpData(IDictionary entryFields, Context ctx { return res; } + res.InternalFields.Add("otp"); string otpUriStart = "otpauth://totp/"; diff --git a/src/keepass2android/Totp/Keepass2TotpPluginAdapter.cs b/src/keepass2android/Totp/Keepass2TotpPluginAdapter.cs index 1340a440d..7b950c38b 100644 --- a/src/keepass2android/Totp/Keepass2TotpPluginAdapter.cs +++ b/src/keepass2android/Totp/Keepass2TotpPluginAdapter.cs @@ -14,14 +14,18 @@ class Keepass2TotpPluginAdapter : ITotpPluginAdapter public TotpData GetTotpData(IDictionary entryFields, Context ctx, bool muteWarnings) { TotpData res = new TotpData(); - byte[] pbSecret = (GetOtpSecret(entryFields, "TimeOtp-") ?? MemUtil.EmptyByteArray); + byte[] pbSecret = (GetOtpSecret(entryFields, "TimeOtp-", out string secretFieldKey) ?? MemUtil.EmptyByteArray); + if (pbSecret.Length == 0) return res; + res.InternalFields.Add(secretFieldKey); + string strPeriod; uint uPeriod = 0; if (entryFields.TryGetValue("TimeOtp-Period", out strPeriod)) { + res.InternalFields.Add("TimeOtp-Period"); uint.TryParse(strPeriod, out uPeriod); } @@ -34,6 +38,7 @@ public TotpData GetTotpData(IDictionary entryFields, Context ctx uint uLength = 0; if (entryFields.TryGetValue("TimeOtp-Length", out strLength)) { + res.InternalFields.Add("TimeOtp-Length"); uint.TryParse(strLength, out uLength); } @@ -42,6 +47,8 @@ public TotpData GetTotpData(IDictionary entryFields, Context ctx string strAlg; entryFields.TryGetValue("TimeOtp-Algorithm", out strAlg); + if (!string.IsNullOrEmpty(strAlg)) + res.InternalFields.Add("TimeOtp-Algorithm"); res.HashAlgorithm = strAlg; res.TotpSecret = pbSecret; @@ -52,32 +59,37 @@ public TotpData GetTotpData(IDictionary entryFields, Context ctx } - private static byte[] GetOtpSecret(IDictionary entryFields, string strPrefix) + private static byte[] GetOtpSecret(IDictionary entryFields, string strPrefix, out string secretFieldKey) { try { string str; - entryFields.TryGetValue(strPrefix + "Secret", out str); - if (!string.IsNullOrEmpty(str)) + secretFieldKey = strPrefix + "Secret"; + entryFields.TryGetValue(secretFieldKey, out str); + if (!string.IsNullOrEmpty(str)) return StrUtil.Utf8.GetBytes(str); - - entryFields.TryGetValue(strPrefix + "Secret-Hex", out str); + + secretFieldKey = strPrefix + "Secret-Hex"; + entryFields.TryGetValue(secretFieldKey, out str); if (!string.IsNullOrEmpty(str)) return MemUtil.HexStringToByteArray(str); - - entryFields.TryGetValue(strPrefix + "Secret-Base32", out str); + + secretFieldKey = strPrefix + "Secret-Base32"; + entryFields.TryGetValue(secretFieldKey, out str); if (!string.IsNullOrEmpty(str)) return Base32.Decode(str); - - entryFields.TryGetValue(strPrefix + "Secret-Base64", out str); + + secretFieldKey = strPrefix + "Secret-Base64"; + entryFields.TryGetValue(secretFieldKey, out str); if (!string.IsNullOrEmpty(str)) return Convert.FromBase64String(str); + } catch (Exception e) { Kp2aLog.LogUnexpectedError(e); } - + secretFieldKey = null; return null; } } diff --git a/src/keepass2android/Totp/Kp2aTotp.cs b/src/keepass2android/Totp/Kp2aTotp.cs index 8e635fa89..1dc15b751 100644 --- a/src/keepass2android/Totp/Kp2aTotp.cs +++ b/src/keepass2android/Totp/Kp2aTotp.cs @@ -10,8 +10,9 @@ namespace keepass2android { class Kp2aTotp { + public const string TotpKey = "TOTP"; - readonly ITotpPluginAdapter[] _pluginAdapters = new ITotpPluginAdapter[] + readonly ITotpPluginAdapter[] _pluginAdapters = new ITotpPluginAdapter[] { new TrayTotpPluginAdapter(), new KeeOtpPluginAdapter(), @@ -46,7 +47,7 @@ public ITotpPluginAdapter TryGetAdapter(PwEntryOutput entry) foreach (ITotpPluginAdapter adapter in _pluginAdapters) { TotpData totpData = adapter.GetTotpData( - App.Kp2a.LastOpenedEntry.OutputStrings.ToDictionary(pair => StrUtil.SafeXmlString(pair.Key), + entry.OutputStrings.ToDictionary(pair => StrUtil.SafeXmlString(pair.Key), pair => pair.Value.ReadString()), LocaleManager.LocalizedAppContext, false); if (totpData.IsTotpEntry) { diff --git a/src/keepass2android/Totp/TotpData.cs b/src/keepass2android/Totp/TotpData.cs index 3d59fb8a4..cca4b214f 100644 --- a/src/keepass2android/Totp/TotpData.cs +++ b/src/keepass2android/Totp/TotpData.cs @@ -31,12 +31,14 @@ public string TotpSeed public string TimeCorrectionUrl { get; set; } public string HashAlgorithm { get; set; } - + public bool IsDefaultRfc6238 { get { return Length == "6" && Duration == "30" && (HashAlgorithm == null || HashAlgorithm == HashSha1); } } + public List InternalFields { get; set; } = new List(); + public static TotpData MakeDefaultRfc6238() { return new TotpData() diff --git a/src/keepass2android/Totp/TrayTotpPluginAdapter.cs b/src/keepass2android/Totp/TrayTotpPluginAdapter.cs index d1266cfa9..54765c95f 100644 --- a/src/keepass2android/Totp/TrayTotpPluginAdapter.cs +++ b/src/keepass2android/Totp/TrayTotpPluginAdapter.cs @@ -106,6 +106,8 @@ public TotpData GetTotpData(IDictionary entryFields) { bool NoTimeCorrection = false; string[] Settings = SettingsGet(entryFields); + res.InternalFields.Add(SettingsFieldName); + res.InternalFields.Add(SeedFieldName); res.Duration = Settings[0]; res.Length = Settings[1]; if (res.Length == "S") diff --git a/src/keepass2android/Totp/UpdateTotpTimerTask.cs b/src/keepass2android/Totp/UpdateTotpTimerTask.cs index 5852af41e..dc34a81ef 100644 --- a/src/keepass2android/Totp/UpdateTotpTimerTask.cs +++ b/src/keepass2android/Totp/UpdateTotpTimerTask.cs @@ -13,7 +13,7 @@ namespace PluginTOTP { class UpdateTotpTimerTask: TimerTask { - public const string TotpKey = "TOTP"; + public const string TotpKey = Kp2aTotp.TotpKey; private readonly Context _context; private readonly ITotpPluginAdapter _adapter; diff --git a/src/keepass2android/app/AppTask.cs b/src/keepass2android/app/AppTask.cs index 3c2b7481c..385d37668 100644 --- a/src/keepass2android/app/AppTask.cs +++ b/src/keepass2android/app/AppTask.cs @@ -6,9 +6,13 @@ using Android.Widget; using System.Collections.Generic; using System.Linq; +using System.Threading; using KeePassLib; using KeePassLib.Security; using KeePassLib.Utility; +using KeeTrayTOTP.Libraries; +using Android.Content.Res; +using Android.Preferences; namespace keepass2android { @@ -339,9 +343,17 @@ protected void RemoveTaskFromIntent(Activity act) } - public virtual void CompleteOnCreateEntryActivity(EntryActivity activity) + public virtual void CompleteOnCreateEntryActivity(EntryActivity activity, Thread notifyPluginsOnOpenThread) { - activity.StartNotificationsService(false); + //this default implementation is executed when we're opening an entry manually, i.e. without search/autofill. + //We only activate the keyboard if this is enabled in "silent mode" + ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(activity); + bool activateKeyboard = prefs.GetBoolean("kp2a_switch_rooted", false) && + !prefs.GetBoolean( + activity.GetString(Resource.String + .OpenKp2aKeyboardAutomaticallyOnlyAfterSearch_key), false); + + activity.StartNotificationsService(activateKeyboard); } public virtual void PopulatePasswordAccessServiceIntent(Intent intent) @@ -353,7 +365,8 @@ public virtual void PopulatePasswordAccessServiceIntent(Intent intent) protected static bool GetBoolFromBundle(Bundle b, string key, bool defaultValue) { bool boolValue; - if (!Boolean.TryParse(b.GetString(key), out boolValue)) + string stringValue = b.GetString(key); + if (!Boolean.TryParse(stringValue, out boolValue)) { boolValue = defaultValue; } @@ -363,7 +376,8 @@ protected static bool GetBoolFromBundle(Bundle b, string key, bool defaultValue) protected static int GetIntFromBundle(Bundle b, string key, int defaultValue) { int intValue; - if (!Int32.TryParse(b.GetString(key), out intValue)) + var strValue = b.GetString(key); + if (!Int32.TryParse(strValue, out intValue)) { intValue = defaultValue; } @@ -383,7 +397,7 @@ public class NullTask: AppTask /// User is about to search an entry for a given URL /// /// Derive from SelectEntryTask. This means that as soon as an Entry is opened, we're returning with - /// ExitAfterTaskComplete. This also allows te specify the flag if we need to display the user notifications. + /// ExitAfterTaskComplete. This also allows to specify the flag if we need to display the user notifications. public class SearchUrlTask: SelectEntryTask { public SearchUrlTask() @@ -392,8 +406,9 @@ public SearchUrlTask() } public const String UrlToSearchKey = "UrlToSearch"; + public const String AutoReturnFromQueryKey = "AutoReturnFromQuery"; - public string UrlToSearchFor + public string UrlToSearchFor { get; set; @@ -416,7 +431,7 @@ public override IEnumerable Extras } } - public const String AutoReturnFromQueryKey = "AutoReturnFromQuery"; + public bool AutoReturnFromQuery { get; set; } @@ -424,15 +439,19 @@ public override void LaunchFirstGroupActivity(Activity act) { if (String.IsNullOrEmpty(UrlToSearchFor)) { - GroupActivity.Launch(act, new SelectEntryTask() { ShowUserNotifications = ShowUserNotifications}, new ActivityLaunchModeRequestCode(0)); + GroupActivity.Launch(act, new SelectEntryTask() { + ShowUserNotifications = ShowUserNotifications, + CopyTotpToClipboard = CopyTotpToClipboard, + ActivateKeyboard = ActivateKeyboard + }, + new ActivityLaunchModeRequestCode(0)); } else { ShareUrlResults.Launch(act, this, new ActivityLaunchModeRequestCode(0)); } - - //removed. this causes an issue in the following workflow: + //removed. this causes an issue in the following workflow: //When the user wants to find an entry for a URL but has the wrong database open he needs //to switch to another database. But the Task is removed already the first time when going through PasswordActivity // (with the wrong db). @@ -453,7 +472,7 @@ public override void PopulatePasswordAccessServiceIntent(Intent intent) intent.PutExtra(UrlToSearchKey, UrlToSearchFor); } - public override void CompleteOnCreateEntryActivity(EntryActivity activity) + public override void CompleteOnCreateEntryActivity(EntryActivity activity, Thread notifyPluginsOnOpenThread) { if (App.Kp2a.LastOpenedEntry != null) App.Kp2a.LastOpenedEntry.SearchUrl = UrlToSearchFor; @@ -462,18 +481,18 @@ public override void CompleteOnCreateEntryActivity(EntryActivity activity) //if the database is readonly (or no URL exists), don't offer to modify the URL if ((App.Kp2a.CurrentDb.CanWrite == false) || (String.IsNullOrEmpty(UrlToSearchFor) || keepass2android.ShareUrlResults.GetSearchResultsForUrl(UrlToSearchFor).Entries.Any(e => e == activity.Entry) )) { - base.CompleteOnCreateEntryActivity(activity); + base.CompleteOnCreateEntryActivity(activity, notifyPluginsOnOpenThread); return; } - AskAddUrlThenCompleteCreate(activity, UrlToSearchFor); + AskAddUrlThenCompleteCreate(activity, UrlToSearchFor, notifyPluginsOnOpenThread); } /// /// brings up a dialog asking the user whether he wants to add the given URL to the entry for automatic finding /// - public void AskAddUrlThenCompleteCreate(EntryActivity activity, string url) + public void AskAddUrlThenCompleteCreate(EntryActivity activity, string url, Thread notifyPluginsOnOpenThread) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.SetTitle(activity.GetString(Resource.String.AddUrlToEntryDialog_title)); @@ -482,12 +501,13 @@ public void AskAddUrlThenCompleteCreate(EntryActivity activity, string url) builder.SetPositiveButton(activity.GetString(Resource.String.yes), (dlgSender, dlgEvt) => { - activity.AddUrlToEntry(url, (EntryActivity thenActiveActivity) => base.CompleteOnCreateEntryActivity(thenActiveActivity)); + activity.AddUrlToEntry(url, (EntryActivity thenActiveActivity) => base.CompleteOnCreateEntryActivity(thenActiveActivity, notifyPluginsOnOpenThread +)); }); builder.SetNegativeButton(activity.GetString(Resource.String.no), (dlgSender, dlgEvt) => { - base.CompleteOnCreateEntryActivity(activity); + base.CompleteOnCreateEntryActivity(activity, notifyPluginsOnOpenThread); }); Dialog dialog = builder.Create(); @@ -495,8 +515,46 @@ public void AskAddUrlThenCompleteCreate(EntryActivity activity, string url) } } + public class OpenSpecificEntryTask : SelectEntryTask + { + public OpenSpecificEntryTask() + { + } + + public const String EntryUuidKey = "EntryUuid"; + + public string EntryUuid + { + get; + set; + } + + public override void Setup(Bundle b) + { + base.Setup(b); + EntryUuid = b.GetString(EntryUuidKey); + + } + public override IEnumerable Extras + { + get + { + foreach (IExtra e in base.Extras) + yield return e; + + yield return new StringExtra { Key = EntryUuidKey, Value = EntryUuid }; + } + } + + public override void LaunchFirstGroupActivity(Activity act) + { + ShareUrlResults.Launch(act, this, new ActivityLaunchModeRequestCode(0)); + } + + } + - public enum ShowUserNotificationsMode + public enum ActivationCondition { Never, WhenTotp, @@ -509,29 +567,34 @@ public class SelectEntryTask: AppTask { public SelectEntryTask() { - ShowUserNotifications = ShowUserNotificationsMode.Always; + ShowUserNotifications = ActivationCondition.Always; CloseAfterCreate = true; - ActivateKeyboard = true; + ActivateKeyboard = ActivationCondition.Never; + CopyTotpToClipboard = false; } public const String ShowUserNotificationsKey = "ShowUserNotifications"; - public ShowUserNotificationsMode ShowUserNotifications { get; set; } + public ActivationCondition ShowUserNotifications { get; set; } public const String CloseAfterCreateKey = "CloseAfterCreate"; public const String ActivateKeyboardKey = "ActivateKeyboard"; + public const String CopyTotpToClipboardKey = "CopyTotpToClipboard"; public bool CloseAfterCreate { get; set; } - public bool ActivateKeyboard { get; set; } + public ActivationCondition ActivateKeyboard { get; set; } + + public bool CopyTotpToClipboard { get; set; } public override void Setup(Bundle b) { - ShowUserNotifications = (ShowUserNotificationsMode) GetIntFromBundle(b, ShowUserNotificationsKey, (int)ShowUserNotificationsMode.Always); + ShowUserNotifications = (ActivationCondition) GetIntFromBundle(b, ShowUserNotificationsKey, (int)ActivationCondition.Always); CloseAfterCreate = GetBoolFromBundle(b, CloseAfterCreateKey, true); - ActivateKeyboard = GetBoolFromBundle(b, ActivateKeyboardKey, true); + ActivateKeyboard = (ActivationCondition)GetIntFromBundle(b, ActivateKeyboardKey, (int)ActivationCondition.Always); + CopyTotpToClipboard = GetBoolFromBundle(b, CopyTotpToClipboardKey, false); } @@ -541,31 +604,59 @@ public override IEnumerable Extras { yield return new StringExtra { Key = ShowUserNotificationsKey, Value = ((int)ShowUserNotifications).ToString() }; yield return new StringExtra { Key = CloseAfterCreateKey, Value = CloseAfterCreate.ToString() }; - yield return new StringExtra { Key = ActivateKeyboardKey, Value = ActivateKeyboard.ToString() }; + yield return new StringExtra { Key = ActivateKeyboardKey, Value = ((int)ActivateKeyboard).ToString() }; + yield return new StringExtra { Key = CopyTotpToClipboardKey, Value = CopyTotpToClipboard.ToString() }; } } - public override void CompleteOnCreateEntryActivity(EntryActivity activity) + public override void CompleteOnCreateEntryActivity(EntryActivity activity, Thread notifyPluginsOnOpenThread) { Context ctx = activity; if (ctx == null) ctx = LocaleManager.LocalizedAppContext; - if ((ShowUserNotifications == ShowUserNotificationsMode.Always) - || ((ShowUserNotifications == ShowUserNotificationsMode.WhenTotp) && new Kp2aTotp().TryGetAdapter(new PwEntryOutput(activity.Entry, App.Kp2a.CurrentDb)) != null)) - { - //show the notifications - activity.StartNotificationsService(ActivateKeyboard); - } + var pwEntryOutput = new PwEntryOutput(activity.Entry, App.Kp2a.CurrentDb); + var totpPluginAdapter = new Kp2aTotp().TryGetAdapter(pwEntryOutput); + bool isTotpEntry = totpPluginAdapter != null; + + bool activateKeyboard = ActivateKeyboard == ActivationCondition.Always || (ActivateKeyboard == ActivationCondition.WhenTotp && isTotpEntry); + + if ((ShowUserNotifications == ActivationCondition.Always) + || ((ShowUserNotifications == ActivationCondition.WhenTotp) && isTotpEntry) + || activateKeyboard) + { + //show the notifications + activity.StartNotificationsService(activateKeyboard); + } else { //to avoid getting into inconsistent state (LastOpenedEntry and Notifications): clear notifications: CopyToClipboardService.CancelNotifications(activity); } - if (CloseAfterCreate) - { - //close - activity.CloseAfterTaskComplete(); + + if (CopyTotpToClipboard && isTotpEntry) + { + Dictionary entryFields = pwEntryOutput.OutputStrings.ToDictionary(pair => StrUtil.SafeXmlString(pair.Key), pair => pair.Value.ReadString()); + var totpData= totpPluginAdapter.GetTotpData(entryFields, activity, true); + if (totpData.IsTotpEntry) + { + TOTPProvider prov = new TOTPProvider(totpData); + string totp = prov.GenerateByByte(totpData.TotpSecret); + CopyToClipboardService.CopyValueToClipboardWithTimeout(activity, totp, true); + + Toast.MakeText(activity, activity.GetString(Resource.String.TotpCopiedToClipboard), + ToastLength.Long).Show(); + } + + + } + + if (CloseAfterCreate) + { + //give plugins and TOTP time to do their work: + notifyPluginsOnOpenThread.Join(TimeSpan.FromSeconds(1)); + //close + activity.CloseAfterTaskComplete(); } } } @@ -629,8 +720,8 @@ public override void SetupGroupBaseActivityButtons(GroupBaseActivity groupBaseAc public class CreateEntryThenCloseTask: AppTask { public CreateEntryThenCloseTask() - { - ShowUserNotifications = ShowUserNotificationsMode.Always; + { + ShowUserNotifications = ActivationCondition.Always; } public override bool CanActivateSearchViewOnStart @@ -670,13 +761,13 @@ public override bool CanActivateSearchViewOnStart public IList ProtectedFieldsList { get; set; } - public ShowUserNotificationsMode ShowUserNotifications { get; set; } + public ActivationCondition ShowUserNotifications { get; set; } public override void Setup(Bundle b) { - ShowUserNotifications = (ShowUserNotificationsMode)GetIntFromBundle(b,ShowUserNotificationsKey, (int)ShowUserNotificationsMode.Always); + ShowUserNotifications = (ActivationCondition)GetIntFromBundle(b,ShowUserNotificationsKey, (int)ActivationCondition.Always); Url = b.GetString(UrlKey); AllFields = b.GetString(AllFieldsKey); @@ -724,15 +815,15 @@ public override void PrepareNewEntry(PwEntry newEntry) public override void AfterAddNewEntry(EntryEditActivity entryEditActivity, PwEntry newEntry) { EntryActivity.Launch(entryEditActivity, newEntry, -1, - new SelectEntryTask { ShowUserNotifications = this.ShowUserNotifications}, + new SelectEntryTask() { ShowUserNotifications = this.ShowUserNotifications, ActivateKeyboard = ActivationCondition.Never }, ActivityFlags.ForwardResult); //no need to call Finish here, that's done in EntryEditActivity ("closeOrShowError") } - public override void CompleteOnCreateEntryActivity(EntryActivity activity) + public override void CompleteOnCreateEntryActivity(EntryActivity activity, Thread notifyPluginsOnOpenThread) { //if the user selects an entry before creating the new one, we're not closing the app - base.CompleteOnCreateEntryActivity(activity); + base.CompleteOnCreateEntryActivity(activity, notifyPluginsOnOpenThread); } } diff --git a/src/keepass2android/keepass2android-app.csproj b/src/keepass2android/keepass2android-app.csproj index dcc1a9126..f51a6854b 100644 --- a/src/keepass2android/keepass2android-app.csproj +++ b/src/keepass2android/keepass2android-app.csproj @@ -1985,6 +1985,9 @@ + + +