Compare commits
11 Commits
2246--fix-
...
totp-impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c354612369 | ||
|
|
4fea731c87 | ||
|
|
e189776ba9 | ||
|
|
31255f0c52 | ||
|
|
059280efd0 | ||
|
|
5edc070aa8 | ||
|
|
be2c28811c | ||
|
|
337e6324ff | ||
|
|
310143c612 | ||
|
|
49cb33a4da | ||
|
|
c934755e1c |
@@ -68,6 +68,9 @@ Please see the [How to use Keepass2Android with YubiKey NEO](How-to-use-Keepass2
|
||||
## Advanced usage of the Keepass2Android keyboard
|
||||
Please see the [Advanced usage of the Keepass2Android keyboard](Advanced-usage-of-the-Keepass2Android-keyboard.md) page.
|
||||
|
||||
## Using Keepass2Android like an authenticator app to generate Time-based One-Time-Passwords (TOTPs)
|
||||
Please see [Generating TOTPs with Keepass2Android](Generating-TOTPs.md)
|
||||
|
||||
# FAQ
|
||||
|
||||
## Should I use the KP2A keyboard for entering passwords?
|
||||
|
||||
53
docs/Generating-TOTPs.md
Normal file
53
docs/Generating-TOTPs.md
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
## TOTP in brief
|
||||
TOTP stands for [Time-based One-Time Password algorithm](https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm) which is one of the most common way proposed by websites to do a [two-factor authentication (2FA)](https://en.wikipedia.org/wiki/Multi-factor_authentication).
|
||||
|
||||
On these websites, this option will often be mentioned in the 2FA configuration menu as things like "_use code generated by an application_", "_use [Google] Authenticator app_".
|
||||
|
||||
You're prompted to scan a QR code with the app, which essentially contains a code called "_seed_", usually with a form like "_AZER TYUI OPQS DFGH JKLM_", used to generate TOTPs. The seed can be also directly copied if there is no scanning option on the app.
|
||||
|
||||
Most common apps:
|
||||
|
||||
- Google Authenticator
|
||||
- Authy
|
||||
- Microsoft Authenticator
|
||||
- FreeOTP
|
||||
- LastPass Authenticator
|
||||
|
||||
## TOTP in KeePass and benefits
|
||||
In KeePass (by Dominik Reichl) there is are several ways to enable this Authenticator app ability:
|
||||
|
||||
- built-in TOTP support: https://keepass.info/help/base/placeholders.html#otp
|
||||
- [KeePassOTP plugin](https://keepass.info/plugins.html#kpotp)
|
||||
- [KeeOtp plugin](https://keepass.info/plugins.html#keeotp)
|
||||
- [KeeTrayTOTP plugin](https://keepass.info/plugins.html#keetraytotp) (note the name "_TrayTOTP_" on this one for later)
|
||||
|
||||
KeePassXC also supports TOTP: https://keepassxc.org/docs/KeePassXC_UserGuide#_adding_totp_to_an_entry
|
||||
|
||||
The greatest benefits are:
|
||||
|
||||
- the seed stays available contrary to the above apps (for which it's more or less hard to backup/restore/switch with another app)
|
||||
- TOTPs are available wherever the KeePass database is available. But conceptually it's not really 2FA anymore (all things are stored in the same place).
|
||||
|
||||
The different implementations use different ways of storing the TOTP seed (or secret, or key) and optional settings (e.g. the length of the TOTP to generate) within an entry inside the kdbx database. Keepass2Android attempts to be able to read the different formats, but can only write one:
|
||||
|
||||
## TOTP in Keepass2Android
|
||||
|
||||
If you use any of the tools mentioned above, you can set up TOTP entries with them. Keepass2Android can read those entries and generate TOTPs if any of the following styles are used:
|
||||
|
||||
* Keepass2 style: used when there are TimeOtp-Secret(-XXX) fields in the entry
|
||||
* KeeOtpPlugin style: used when there is an otp field containing a query string in the form of key=abc&step=X&size=Y (step and size are optional)
|
||||
* KeeWebOtp/Key Uri Format style: used when entry contains a URL starting with otpauth://totp/, e.g. otpauth://totp/?secret=abc (https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
|
||||
* KeeTrayTotp style:
|
||||
* requires a non-empty seed field (default key is "TOTP seed", can be changed in KP2A settings), value is base32 encoded data
|
||||
* requires a non-empty settings field (default key is "TOTP Settings", can be changed as well), value is expected to be a csv-separated array with [Duration];Length(;TimeCorrectionURL). Length is either an integer value or "S" to indicate Steam encoding
|
||||
|
||||
In order to view the generated TOTP code in KP2A, open the corresponding entry. You can then
|
||||
* use a dynamically generated field called "_TOTP_" containing the TOTP or
|
||||
* use the "Copy TOTP" button on the system notification for the selected entry or
|
||||
* switch to the KP2A keyboard and use the TOTP button to insert the TOTP value into the target app or browser
|
||||
|
||||
If you want to configure an entry to contain the TOTP fields, it is suggested to enter edit mode for the entry. Then click the "Configure TOTP" button. You can either enter the data manually or scan a QR code with the information.
|
||||
|
||||
### Spaces in otp field
|
||||
Make sure that the URI doesn't contain spaces, otherwise KeePass2Android will fail to generate TOTPs as a space is an invalid character. If your URIs have spaces, check [this comment](https://github.com/PhilippC/keepass2android/issues/1248#issuecomment-628035961)._
|
||||
@@ -91,7 +91,29 @@ namespace keepass2android
|
||||
|
||||
}
|
||||
|
||||
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<PwEntry> listResults = pgResults.Entries;
|
||||
|
||||
database.Root.SearchEntries(sp, listResults, new NullStatusLogger());
|
||||
|
||||
return pgResults;
|
||||
|
||||
}
|
||||
|
||||
private static String ExtractHost(String url)
|
||||
{
|
||||
return UrlUtil.GetHost(url.Trim());
|
||||
}
|
||||
|
||||
@@ -174,10 +174,17 @@ namespace keepass2android
|
||||
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;
|
||||
|
||||
@@ -32,6 +32,7 @@ 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;
|
||||
@@ -49,7 +50,9 @@ 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;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
@@ -286,6 +289,8 @@ namespace keepass2android
|
||||
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 @@ namespace keepass2android
|
||||
_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 @@ namespace keepass2android
|
||||
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 @@ namespace keepass2android
|
||||
{
|
||||
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 @@ namespace keepass2android
|
||||
|
||||
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<int> { Resource.Id.entry_password, Resource.Id.entry_password_visible}, Resource.Id.entryfield_container_password, PwDefs.PasswordField);
|
||||
PopulateStandardText(new List<int> { Resource.Id.entry_totp, Resource.Id.entry_totp_visible }, Resource.Id.entryfield_container_totp, Kp2aTotp.TotpKey);
|
||||
PopulateStandardText(new List<int> { Resource.Id.entry_password, Resource.Id.entry_password_visible}, Resource.Id.entryfield_container_password, PwDefs.PasswordField);
|
||||
|
||||
RegisterProtectedTextView(PwDefs.PasswordField, FindViewById<TextView>(Resource.Id.entry_password), FindViewById<TextView>(Resource.Id.entry_password_visible));
|
||||
RegisterProtectedTextView(Kp2aTotp.TotpKey, FindViewById<TextView>(Resource.Id.entry_totp), FindViewById<TextView>(Resource.Id.entry_totp_visible));
|
||||
|
||||
RegisterTextPopup(FindViewById<RelativeLayout> (Resource.Id.groupname_container),
|
||||
RegisterTextPopup(FindViewById<RelativeLayout> (Resource.Id.groupname_container),
|
||||
FindViewById (Resource.Id.entry_group_name), KeyGroupFullPath);
|
||||
|
||||
RegisterTextPopup(FindViewById<RelativeLayout>(Resource.Id.username_container),
|
||||
@@ -961,9 +969,11 @@ namespace keepass2android
|
||||
.Add(new GotoUrlMenuItem(this, PwDefs.UrlField));
|
||||
RegisterTextPopup(FindViewById<RelativeLayout>(Resource.Id.password_container),
|
||||
FindViewById(Resource.Id.password_vdots), PwDefs.PasswordField);
|
||||
RegisterTextPopup(FindViewById<RelativeLayout>(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 @@ namespace keepass2android
|
||||
|
||||
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<ProgressBar>(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 @@ namespace keepass2android
|
||||
}
|
||||
private List<IPopupMenuItem> 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<IPopupMenuItem> RegisterTextPopup(View container, View anchor, string fieldKey, bool isProtected)
|
||||
@@ -1055,7 +1099,12 @@ namespace keepass2android
|
||||
popupItems.Add(new CopyToClipboardPopupMenuIcon(this, _stringViews[fieldKey], isProtected));
|
||||
if (isProtected)
|
||||
{
|
||||
var valueView = container.FindViewById<TextView>(fieldKey == PwDefs.PasswordField ? Resource.Id.entry_password : Resource.Id.entry_extra);
|
||||
var valueView = container.FindViewById<TextView>(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 @@ namespace keepass2android
|
||||
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 @@ namespace keepass2android
|
||||
ClearCache();
|
||||
base.OnResume();
|
||||
_activityDesign.ReapplyTheme();
|
||||
}
|
||||
isPaused = false;
|
||||
Task.Run(UpdateTotpCountdown);
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace keepass2android
|
||||
//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;
|
||||
|
||||
BIN
src/keepass2android/Resources/drawable-mdpi/ic_entry_totp.png
Normal file
BIN
src/keepass2android/Resources/drawable-mdpi/ic_entry_totp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src/keepass2android/Resources/drawable-xhdpi/ic_entry_totp.png
Normal file
BIN
src/keepass2android/Resources/drawable-xhdpi/ic_entry_totp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
@@ -184,6 +184,68 @@
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/entryfield_container_totp"
|
||||
style="@style/EntryEditSingleLine_container">
|
||||
<ImageView
|
||||
style="@style/EntryEditSingleLine_ImageView"
|
||||
android:src="@drawable/ic_entry_totp" />
|
||||
|
||||
<LinearLayout
|
||||
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:orientation="vertical">
|
||||
<!-- TOTP -->
|
||||
<TextView
|
||||
android:id="@+id/entry_totp_label"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/TOTP"
|
||||
style="@style/EntryFieldHeader" />
|
||||
<RelativeLayout
|
||||
android:id="@+id/totp_container"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:orientation="horizontal"
|
||||
android:clickable="true"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
<ImageView
|
||||
android:id="@+id/totp_vdots"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="15dp"
|
||||
android:src="@drawable/vdots"
|
||||
android:gravity="right|bottom"
|
||||
android:layout_alignParentRight="true" />
|
||||
<TextView
|
||||
android:id="@+id/entry_totp"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:password="true"
|
||||
android:typeface="monospace"
|
||||
android:layout_toLeftOf="@id/totp_vdots"
|
||||
style="@style/EntryItem" />
|
||||
<TextView
|
||||
android:id="@+id/entry_totp_visible"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toLeftOf="@id/totp_vdots"
|
||||
style="@style/EntryItem" />
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/TotpCountdownProgressBar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginRight="30dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/entryfield_container_comment"
|
||||
style="@style/EntryEditSingleLine_container">
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
<string name="oi_filemanager_web">https://openintents.googlecode.com/files/FileManager-2.0.2.apk</string>
|
||||
<string name="permission_desc2">KP2A Search</string>
|
||||
<string name="permission_desc3">KP2A Choose autofill dataset</string>
|
||||
<string name="AutoFillTotp_prefs_screen_key">AutoFillTotp_prefs_screen_key</string>
|
||||
|
||||
|
||||
<!-- Preference settings -->
|
||||
|
||||
@@ -402,6 +402,16 @@
|
||||
<string name="ShowSeparateNotifications_summary">Show separate notifications for copying username and password to clipboard and activating the keyboard.</string>
|
||||
<string name="AccServiceAutoFill_prefs">AutoFill Accessibility-Service</string>
|
||||
<string name="AutoFill_prefs">AutoFill Service</string>
|
||||
<string name="AutoFillTotp_prefs_ShowNotification_summary">When autofilling an entry with TOTP, show the entry notification with a Copy TOTP button</string>
|
||||
<string name="AutoFillTotp_prefs_ShowNotification_title">Show entry notification</string>
|
||||
<string name="AutoFillTotp_prefs_title">Autofill for TOTP entries</string>
|
||||
<string name="AutoFillTotp_prefs_CopyTotpToClipboard_title">Copy TOTP to clipboard</string>
|
||||
<string name="AutoFillTotp_prefs_CopyTotpToClipboard_summary">When autofilling an entry with TOTP, copy the TOTP to the clipboard</string>
|
||||
<string name="AutoFillTotp_prefs_ActivateKeyboard_summary">When autofilling an entry with TOTP, activate the built-in keyboard. The keyboard has a TOTP button.</string>
|
||||
<string name="AutoFillTotp_prefs_ActivateKeyboard_title">Activate built-in keyboard</string>
|
||||
|
||||
<string name="TotpCopiedToClipboard">Copied TOTP to clipboard</string>
|
||||
|
||||
<string name="ShowKp2aKeyboardNotification_title">KP2A keyboard notification</string>
|
||||
<string name="ShowKp2aKeyboardNotification_summary">Make full entry accessible through the KP2A keyboard (recommended).</string>
|
||||
<string name="OpenKp2aKeyboardAutomatically_title">Switch keyboard</string>
|
||||
@@ -589,6 +599,7 @@
|
||||
<string name="CouldntLoadChalAuxFile_Hint">Please use the KeeChallenge plugin in KeePass 2.x (PC) to configure your database for use with challenge-response!</string>
|
||||
<string name="ErrorUpdatingChalAuxFile">Error updating OTP auxiliary file!</string>
|
||||
<string name="TrayTotp_SeedField_title">TOTP Seed field name</string>
|
||||
<string name="TOTP">TOTP</string>
|
||||
<string name="TrayTotp_SeedField_summary">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.</string>
|
||||
<string name="TrayTotp_SettingsField_title">TOTP Settings field name</string>
|
||||
<string name="TrayTotp_SettingsField_summary">Enter the field name of the settings field for TrayTotp here.</string>
|
||||
|
||||
@@ -461,7 +461,8 @@
|
||||
android:defaultValue="false"
|
||||
android:title="@string/LogAutofillView_title"
|
||||
android:key="@string/LogAutofillView_key" />
|
||||
|
||||
|
||||
|
||||
|
||||
<CheckBoxPreference
|
||||
android:enabled="true"
|
||||
@@ -476,6 +477,38 @@
|
||||
android:summary="@string/AutofillDisabledQueriesPreference_summary"
|
||||
android:persistent="false"
|
||||
android:key="AutofillDisabledQueriesPreference_key"/>
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="@string/AutoFillTotp_prefs_screen_key"
|
||||
android:title="@string/AutoFillTotp_prefs_title"
|
||||
>
|
||||
<keepass2android.ToolbarPreference
|
||||
android:key="@string/AutoFillTotp_prefs_screen_key"
|
||||
android:title="@string/AutoFillTotp_prefs_title" />
|
||||
|
||||
<CheckBoxPreference android:key="AutoFillTotp_prefs_ShowNotification_key"
|
||||
android:enabled="true"
|
||||
android:persistent="true"
|
||||
android:summary="@string/AutoFillTotp_prefs_ShowNotification_summary"
|
||||
android:defaultValue="true"
|
||||
android:title="@string/AutoFillTotp_prefs_ShowNotification_title"
|
||||
/>
|
||||
<CheckBoxPreference android:key="AutoFillTotp_prefs_CopyTotpToClipboard_key"
|
||||
android:enabled="true"
|
||||
android:persistent="true"
|
||||
android:summary="@string/AutoFillTotp_prefs_CopyTotpToClipboard_summary"
|
||||
android:defaultValue="true"
|
||||
android:title="@string/AutoFillTotp_prefs_CopyTotpToClipboard_title"
|
||||
/>
|
||||
<CheckBoxPreference android:key="AutoFillTotp_prefs_ActivateKeyboard_key"
|
||||
android:enabled="true"
|
||||
android:persistent="true"
|
||||
android:summary="@string/AutoFillTotp_prefs_ActivateKeyboard_summary"
|
||||
android:defaultValue="false"
|
||||
android:title="@string/AutoFillTotp_prefs_ActivateKeyboard_title"
|
||||
/>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
@@ -306,7 +307,27 @@ namespace keepass2android
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,12 @@ namespace keepass2android
|
||||
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 @@ namespace keepass2android
|
||||
//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 @@ namespace keepass2android
|
||||
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 @@ namespace keepass2android
|
||||
}
|
||||
|
||||
//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 @@ namespace keepass2android
|
||||
FragmentManager.FindFragmentById<GroupListFragment>(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 @@ namespace keepass2android
|
||||
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));
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using Android.Content;
|
||||
using keepass2android;
|
||||
using KeePassLib.Collections;
|
||||
|
||||
namespace PluginTOTP
|
||||
@@ -39,23 +40,34 @@ namespace PluginTOTP
|
||||
{
|
||||
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];
|
||||
|
||||
try
|
||||
{
|
||||
res.TotpSeed = parameters[KeyParameter];
|
||||
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
res.Duration = GetIntOrDefault(parameters, StepParameter, 30).ToString();
|
||||
res.Length = GetIntOrDefault(parameters, SizeParameter, 6).ToString();
|
||||
|
||||
res.IsTotpEntry = true;
|
||||
return res;
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace keepass2android
|
||||
{
|
||||
return res;
|
||||
}
|
||||
res.InternalFields.Add("otp");
|
||||
|
||||
string otpUriStart = "otpauth://totp/";
|
||||
|
||||
|
||||
@@ -14,14 +14,18 @@ namespace PluginTOTP
|
||||
public TotpData GetTotpData(IDictionary<string, string> 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 @@ namespace PluginTOTP
|
||||
uint uLength = 0;
|
||||
if (entryFields.TryGetValue("TimeOtp-Length", out strLength))
|
||||
{
|
||||
res.InternalFields.Add("TimeOtp-Length");
|
||||
uint.TryParse(strLength, out uLength);
|
||||
}
|
||||
|
||||
@@ -42,6 +47,8 @@ namespace PluginTOTP
|
||||
|
||||
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 @@ namespace PluginTOTP
|
||||
}
|
||||
|
||||
|
||||
private static byte[] GetOtpSecret(IDictionary<string, string> entryFields, string strPrefix)
|
||||
private static byte[] GetOtpSecret(IDictionary<string, string> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@ namespace keepass2android
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -31,12 +31,14 @@ namespace PluginTOTP
|
||||
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<string> InternalFields { get; set; } = new List<string>();
|
||||
|
||||
public static TotpData MakeDefaultRfc6238()
|
||||
{
|
||||
return new TotpData()
|
||||
|
||||
@@ -31,17 +31,8 @@ namespace PluginTOTP
|
||||
_muteWarnings = muteWarnings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if specified Entry contains Settings that are not null.
|
||||
/// </summary>
|
||||
internal bool SettingsCheck(IDictionary<string, string> entryFields)
|
||||
{
|
||||
string settings;
|
||||
entryFields.TryGetValue(SettingsFieldName, out settings);
|
||||
return !String.IsNullOrEmpty(settings);
|
||||
}
|
||||
|
||||
internal bool SeedCheck(IDictionary<string, string> entryFields)
|
||||
|
||||
internal bool HasSeed(IDictionary<string, string> entryFields)
|
||||
{
|
||||
string seed;
|
||||
entryFields.TryGetValue(SeedFieldName, out seed);
|
||||
@@ -100,21 +91,23 @@ namespace PluginTOTP
|
||||
}
|
||||
|
||||
private string[] SettingsGet(IDictionary<string, string> entryFields)
|
||||
{
|
||||
return entryFields[SettingsFieldName].Split(';');
|
||||
}
|
||||
{
|
||||
return entryFields.TryGetValue(SettingsFieldName, out var settings) ? settings.Split(';') : new[] { "30", "6" };
|
||||
}
|
||||
|
||||
public TotpData GetTotpData(IDictionary<string, string> entryFields)
|
||||
{
|
||||
TotpData res = new TotpData();
|
||||
|
||||
if (SettingsCheck(entryFields) && SeedCheck(entryFields))
|
||||
if (HasSeed(entryFields))
|
||||
{
|
||||
bool ValidInterval; bool ValidLength; bool ValidUrl;
|
||||
if (SettingsValidate(entryFields, out ValidInterval, out ValidLength, out ValidUrl))
|
||||
{
|
||||
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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -6,9 +6,13 @@ using Android.OS;
|
||||
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 @@ namespace keepass2android
|
||||
|
||||
}
|
||||
|
||||
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 @@ namespace keepass2android
|
||||
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 @@ namespace keepass2android
|
||||
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 @@ namespace keepass2android
|
||||
/// User is about to search an entry for a given URL
|
||||
/// </summary>
|
||||
/// 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 @@ namespace keepass2android
|
||||
}
|
||||
|
||||
public const String UrlToSearchKey = "UrlToSearch";
|
||||
public const String AutoReturnFromQueryKey = "AutoReturnFromQuery";
|
||||
|
||||
public string UrlToSearchFor
|
||||
public string UrlToSearchFor
|
||||
{
|
||||
get;
|
||||
set;
|
||||
@@ -416,7 +431,7 @@ namespace keepass2android
|
||||
}
|
||||
}
|
||||
|
||||
public const String AutoReturnFromQueryKey = "AutoReturnFromQuery";
|
||||
|
||||
|
||||
public bool AutoReturnFromQuery { get; set; }
|
||||
|
||||
@@ -424,15 +439,19 @@ namespace keepass2android
|
||||
{
|
||||
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 @@ namespace keepass2android
|
||||
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 @@ namespace keepass2android
|
||||
//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);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// brings up a dialog asking the user whether he wants to add the given URL to the entry for automatic finding
|
||||
/// </summary>
|
||||
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 @@ namespace keepass2android
|
||||
|
||||
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 @@ namespace keepass2android
|
||||
}
|
||||
}
|
||||
|
||||
public class OpenSpecificEntryTask : SelectEntryTask
|
||||
{
|
||||
public OpenSpecificEntryTask()
|
||||
{
|
||||
}
|
||||
|
||||
public enum ShowUserNotificationsMode
|
||||
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<IExtra> 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 ActivationCondition
|
||||
{
|
||||
Never,
|
||||
WhenTotp,
|
||||
@@ -509,29 +567,34 @@ namespace keepass2android
|
||||
{
|
||||
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 @@ namespace keepass2android
|
||||
{
|
||||
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<string, string> 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 @@ namespace keepass2android
|
||||
public class CreateEntryThenCloseTask: AppTask
|
||||
{
|
||||
public CreateEntryThenCloseTask()
|
||||
{
|
||||
ShowUserNotifications = ShowUserNotificationsMode.Always;
|
||||
{
|
||||
ShowUserNotifications = ActivationCondition.Always;
|
||||
}
|
||||
|
||||
public override bool CanActivateSearchViewOnStart
|
||||
@@ -670,13 +761,13 @@ namespace keepass2android
|
||||
|
||||
public IList<string> 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 @@ namespace keepass2android
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1985,6 +1985,9 @@
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-mdpi\ic_storage_pcloudall.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\ic_entry_totp.png" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
|
||||
@@ -20,6 +20,7 @@ using AndroidX.AutoFill.Inline;
|
||||
using AndroidX.AutoFill.Inline.V1;
|
||||
using Java.Util.Concurrent.Atomic;
|
||||
using keepass2android.services.AutofillBase.model;
|
||||
using KeePassLib;
|
||||
using Kp2aAutofillParser;
|
||||
|
||||
namespace keepass2android.services.AutofillBase
|
||||
@@ -29,7 +30,7 @@ namespace keepass2android.services.AutofillBase
|
||||
PendingIntent GetAuthPendingIntentForResponse(Context context, string query, string queryDomain, string queryPackage,
|
||||
bool isManualRequest, bool autoReturnFromQuery, AutofillServiceBase.DisplayWarning warning);
|
||||
|
||||
PendingIntent GetAuthPendingIntentForWarning(Context context, string query, string queryDomain, string queryPackage, AutofillServiceBase.DisplayWarning warning);
|
||||
PendingIntent GetAuthPendingIntentForWarning(Context context, PwUuid entryUuid, AutofillServiceBase.DisplayWarning warning);
|
||||
|
||||
PendingIntent GetDisablePendingIntentForResponse(Context context, string query,
|
||||
bool isManualRequest, bool isDisable);
|
||||
@@ -262,33 +263,42 @@ namespace keepass2android.services.AutofillBase
|
||||
{
|
||||
List<Dataset> result = new List<Dataset>();
|
||||
Kp2aLog.Log("AF: BuildEntryDatasets");
|
||||
var suggestedEntries = GetSuggestedEntries(query).ToDictionary(e => e.DatasetName, e => e);
|
||||
Dictionary<PwEntryOutput, FilledAutofillFieldCollection<ViewNodeInputField>> suggestedEntries = GetSuggestedEntries(query);
|
||||
Kp2aLog.Log("AF: BuildEntryDatasets found " + suggestedEntries.Count + " entries");
|
||||
|
||||
int count = 0;
|
||||
foreach (var filledAutofillFieldCollection in suggestedEntries.Values)
|
||||
|
||||
var totpHelper = new Kp2aTotp();
|
||||
|
||||
foreach (var kvp in suggestedEntries)
|
||||
{
|
||||
var filledAutofillFieldCollection = kvp.Value;
|
||||
PwEntryOutput entry = kvp.Key;
|
||||
|
||||
if (filledAutofillFieldCollection == null)
|
||||
continue;
|
||||
|
||||
var inlinePresentationSpec = AutofillHelper.ExtractSpec(inlinePresentationSpecs, count);
|
||||
|
||||
if (warning == DisplayWarning.None)
|
||||
if ((warning == DisplayWarning.None)
|
||||
&& (totpHelper.TryGetAdapter(entry) == null))
|
||||
{
|
||||
|
||||
//no special dataset, we can immediately return the field collection
|
||||
FilledAutofillFieldCollection<ViewNodeInputField> partitionData =
|
||||
AutofillHintsHelper.FilterForPartition(filledAutofillFieldCollection, parser.AutofillFields.FocusedAutofillCanonicalHints);
|
||||
|
||||
Kp2aLog.Log("AF: Add dataset");
|
||||
|
||||
result.Add(AutofillHelper.NewDataset(this, parser.AutofillFields, partitionData, IntentBuilder,
|
||||
result.Add(AutofillHelper.NewDataset(this, parser.AutofillFields, partitionData, IntentBuilder,
|
||||
inlinePresentationSpec));
|
||||
}
|
||||
else
|
||||
{
|
||||
//return an "auth" dataset (actually for just warning the user in case domain/package dont match)
|
||||
|
||||
//return an "auth" dataset (actually for just warning the user in case domain/package dont match and/or to make sure that we open the EntryActivity,
|
||||
// thus opening the entry notification in case of TOTP)
|
||||
PendingIntent pendingIntent =
|
||||
IntentBuilder.GetAuthPendingIntentForWarning(this, query, queryDomain, queryPackage, warning);
|
||||
IntentBuilder.GetAuthPendingIntentForWarning(this, entry.Uuid, warning);
|
||||
var datasetName = filledAutofillFieldCollection.DatasetName;
|
||||
if (datasetName == null)
|
||||
{
|
||||
@@ -320,7 +330,7 @@ namespace keepass2android.services.AutofillBase
|
||||
|
||||
}
|
||||
|
||||
protected abstract List<FilledAutofillFieldCollection<ViewNodeInputField>> GetSuggestedEntries(string query);
|
||||
protected abstract Dictionary<PwEntryOutput, FilledAutofillFieldCollection<ViewNodeInputField>> GetSuggestedEntries(string query);
|
||||
|
||||
public enum DisplayWarning
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace keepass2android.services.AutofillBase
|
||||
{
|
||||
protected Intent ReplyIntent;
|
||||
|
||||
|
||||
public static string ExtraUuidString => "EXTRA_UUID_STRING";
|
||||
public static string ExtraQueryString => "EXTRA_QUERY_STRING";
|
||||
public static string ExtraQueryPackageString => "EXTRA_QUERY_PACKAGE_STRING";
|
||||
public static string ExtraQueryDomainString => "EXTRA_QUERY_DOMAIN_STRING";
|
||||
@@ -50,9 +50,10 @@ namespace keepass2android.services.AutofillBase
|
||||
}
|
||||
|
||||
string requestedUrl = Intent.GetStringExtra(ExtraQueryString);
|
||||
if (requestedUrl == null)
|
||||
string requestedUuid = Intent.GetStringExtra(ExtraUuidString);
|
||||
if (requestedUrl == null && requestedUuid == null)
|
||||
{
|
||||
Kp2aLog.Log("ChooseForAutofillActivityBase: no requestedUrl ");
|
||||
Kp2aLog.Log("ChooseForAutofillActivityBase: no requestedUrl and no requestedUuid");
|
||||
Toast.MakeText(this, "Cannot execute query for null.", ToastLength.Long).Show();
|
||||
RestartApp();
|
||||
return;
|
||||
@@ -134,18 +135,30 @@ namespace keepass2android.services.AutofillBase
|
||||
private void Proceed()
|
||||
{
|
||||
string requestedUrl = Intent.GetStringExtra(ExtraQueryString);
|
||||
string requestedUuid = Intent.GetStringExtra(ExtraUuidString);
|
||||
|
||||
var i = GetQueryIntent(requestedUrl, Intent.GetBooleanExtra(ExtraAutoReturnFromQuery, true), Intent.GetBooleanExtra(ExtraUseLastOpenedEntry, false));
|
||||
if (i == null)
|
||||
if (requestedUuid != null)
|
||||
{
|
||||
//GetQueryIntent returns null if no query is required
|
||||
ReturnSuccess();
|
||||
var i = GetOpenEntryIntent(requestedUuid);
|
||||
StartActivityForResult(i, RequestCodeQuery);
|
||||
}
|
||||
else
|
||||
StartActivityForResult(i, RequestCodeQuery);
|
||||
{
|
||||
var i = GetQueryIntent(requestedUrl, Intent.GetBooleanExtra(ExtraAutoReturnFromQuery, true), Intent.GetBooleanExtra(ExtraUseLastOpenedEntry, false));
|
||||
if (i == null)
|
||||
{
|
||||
//GetQueryIntent returns null if no query is required
|
||||
ReturnSuccess();
|
||||
}
|
||||
else
|
||||
StartActivityForResult(i, RequestCodeQuery);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected abstract Intent GetQueryIntent(string requestedUrl, bool autoReturnFromQuery, bool useLastOpenedEntry);
|
||||
protected abstract Intent GetOpenEntryIntent(string entryUuid);
|
||||
|
||||
protected void RestartApp()
|
||||
{
|
||||
|
||||
@@ -501,24 +501,8 @@ namespace keepass2android
|
||||
if (hasKeyboardDataNow)
|
||||
{
|
||||
notBuilder.AddKeyboardAccess();
|
||||
if (prefs.GetBoolean("kp2a_switch_rooted", false))
|
||||
{
|
||||
//switch rooted
|
||||
bool onlySwitchOnSearch = prefs.GetBoolean(GetString(Resource.String.OpenKp2aKeyboardAutomaticallyOnlyAfterSearch_key), false);
|
||||
if (activateKeyboard || (!onlySwitchOnSearch))
|
||||
{
|
||||
ActivateKp2aKeyboard();
|
||||
}
|
||||
}
|
||||
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 ((activateKeyboard) && prefs.GetBoolean(GetString(Resource.String.OpenKp2aKeyboardAutomatically_key), Resources.GetBoolean(Resource.Boolean.OpenKp2aKeyboardAutomatically_default)))
|
||||
{
|
||||
ActivateKp2aKeyboard();
|
||||
}
|
||||
}
|
||||
if (activateKeyboard)
|
||||
ActivateKp2aKeyboard();
|
||||
|
||||
}
|
||||
|
||||
@@ -548,31 +532,6 @@ namespace keepass2android
|
||||
|
||||
}
|
||||
|
||||
public void ActivateKeyboardIfAppropriate(bool closeAfterCreate, ISharedPreferences prefs)
|
||||
{
|
||||
if (prefs.GetBoolean("kp2a_switch_rooted", false))
|
||||
{
|
||||
//switch rooted
|
||||
bool onlySwitchOnSearch = prefs.GetBoolean(
|
||||
GetString(Resource.String.OpenKp2aKeyboardAutomaticallyOnlyAfterSearch_key), false);
|
||||
if (closeAfterCreate || (!onlySwitchOnSearch))
|
||||
{
|
||||
ActivateKp2aKeyboard();
|
||||
}
|
||||
}
|
||||
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 ((closeAfterCreate) &&
|
||||
prefs.GetBoolean(GetString(Resource.String.OpenKp2aKeyboardAutomatically_key),
|
||||
Resources.GetBoolean(Resource.Boolean.OpenKp2aKeyboardAutomatically_default)))
|
||||
{
|
||||
ActivateKp2aKeyboard();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool ClearNotifications()
|
||||
{
|
||||
// Notification Manager
|
||||
@@ -587,6 +546,7 @@ namespace keepass2android
|
||||
return hadKeyboardData;
|
||||
}
|
||||
|
||||
|
||||
bool MakeAccessibleForKeyboard(PwEntryOutput entry, string searchUrl)
|
||||
{
|
||||
#if EXCLUDE_KEYBOARD
|
||||
@@ -595,38 +555,41 @@ namespace keepass2android
|
||||
bool hasData = false;
|
||||
Keepass2android.Kbbridge.KeyboardDataBuilder kbdataBuilder = new Keepass2android.Kbbridge.KeyboardDataBuilder();
|
||||
|
||||
String[] keys = {PwDefs.UserNameField,
|
||||
String[] standardKeys = {PwDefs.UserNameField,
|
||||
PwDefs.PasswordField,
|
||||
Kp2aTotp.TotpKey,
|
||||
PwDefs.UrlField,
|
||||
PwDefs.NotesField,
|
||||
PwDefs.TitleField
|
||||
};
|
||||
int[] resIds = {Resource.String.entry_user_name,
|
||||
Resource.String.entry_password,
|
||||
0,
|
||||
Resource.String.entry_url,
|
||||
Resource.String.entry_comment,
|
||||
Resource.String.entry_title };
|
||||
|
||||
//add standard fields:
|
||||
int i = 0;
|
||||
foreach (string key in keys)
|
||||
foreach (string key in standardKeys)
|
||||
{
|
||||
String value = entry.OutputStrings.ReadSafe(key);
|
||||
|
||||
if (value.Length > 0)
|
||||
{
|
||||
kbdataBuilder.AddString(key, GetString(resIds[i]), value);
|
||||
kbdataBuilder.AddString(key, resIds[i] > 0 ? GetString(resIds[i]) : key, value);
|
||||
hasData = true;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
//add additional fields:
|
||||
var totpData = new Kp2aTotp().TryGetTotpData(entry);
|
||||
foreach (var pair in entry.OutputStrings)
|
||||
{
|
||||
var key = pair.Key;
|
||||
var value = pair.Value.ReadString();
|
||||
|
||||
if (!PwDefs.IsStandardField(key))
|
||||
if (!standardKeys.Contains(key) && totpData?.InternalFields.Contains(key) != true)
|
||||
{
|
||||
kbdataBuilder.AddString(pair.Key, pair.Key, value);
|
||||
hasData = true;
|
||||
@@ -865,14 +828,9 @@ namespace keepass2android
|
||||
{
|
||||
//let's bring up the keyboard switching dialog.
|
||||
//Unfortunately this no longer works starting with Android 9 if our app is not in foreground.
|
||||
bool mustUseHelperActivity = false;
|
||||
if ((int)Build.VERSION.SdkInt >= 28)
|
||||
{
|
||||
ActivityManager.RunningAppProcessInfo appProcessInfo = new ActivityManager.RunningAppProcessInfo();
|
||||
ActivityManager.GetMyMemoryState(appProcessInfo);
|
||||
//at least on Samsung devices, we always need the helper activity
|
||||
mustUseHelperActivity = (appProcessInfo.Importance != Importance.Foreground) || (Build.Manufacturer != "Google");
|
||||
}
|
||||
//first it seemed to be required for Samsung mostly, but there are use cases where it is required for other devices as well.
|
||||
//Let's be sure and use the helper activity.
|
||||
bool mustUseHelperActivity = (int)Build.VERSION.SdkInt >= 28;
|
||||
if (mustUseHelperActivity)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -10,6 +10,7 @@ using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using AndroidX.Preference;
|
||||
using KeePass.Util.Spr;
|
||||
using keepass2android.services.AutofillBase;
|
||||
using keepass2android.services.AutofillBase.model;
|
||||
@@ -26,6 +27,32 @@ namespace keepass2android.services.Kp2aAutofill
|
||||
Permission = "keepass2android." + AppNames.PackagePart + ".permission.Kp2aChooseAutofill")]
|
||||
public class ChooseForAutofillActivity : ChooseForAutofillActivityBase
|
||||
{
|
||||
public bool ActivateKeyboardWhenTotpPreference
|
||||
{
|
||||
get
|
||||
{
|
||||
return PreferenceManager.GetDefaultSharedPreferences(this)
|
||||
.GetBoolean("AutoFillTotp_prefs_ActivateKeyboard_key", false);
|
||||
}
|
||||
}
|
||||
public bool CopyTotpToClipboardPreference
|
||||
{
|
||||
get
|
||||
{
|
||||
return PreferenceManager.GetDefaultSharedPreferences(this)
|
||||
.GetBoolean("AutoFillTotp_prefs_CopyTotpToClipboard_key", true);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowNotificationPreference
|
||||
{
|
||||
get
|
||||
{
|
||||
return PreferenceManager.GetDefaultSharedPreferences(this)
|
||||
.GetBoolean("AutoFillTotp_prefs_ShowNotification_key", true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override Intent GetQueryIntent(string requestedUrl, bool autoReturnFromQuery, bool useLastOpenedEntry)
|
||||
{
|
||||
if (useLastOpenedEntry && (App.Kp2a.LastOpenedEntry?.SearchUrl == requestedUrl))
|
||||
@@ -36,7 +63,33 @@ namespace keepass2android.services.Kp2aAutofill
|
||||
//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, AutoReturnFromQuery = autoReturnFromQuery, ActivateKeyboard = false };
|
||||
var task = new SearchUrlTask()
|
||||
{
|
||||
UrlToSearchFor = requestedUrl,
|
||||
AutoReturnFromQuery = autoReturnFromQuery
|
||||
};
|
||||
SetTotpDependantActionsOnTask(task);
|
||||
|
||||
task.ToIntent(i);
|
||||
return i;
|
||||
}
|
||||
|
||||
private void SetTotpDependantActionsOnTask(SelectEntryTask task)
|
||||
{
|
||||
task.ShowUserNotifications =
|
||||
ShowNotificationPreference ? ActivationCondition.WhenTotp : ActivationCondition.Never;
|
||||
task.CopyTotpToClipboard = CopyTotpToClipboardPreference;
|
||||
task.ActivateKeyboard = ActivateKeyboardWhenTotpPreference
|
||||
? ActivationCondition.WhenTotp
|
||||
: ActivationCondition.Never;
|
||||
}
|
||||
|
||||
protected override Intent GetOpenEntryIntent(string entryUuid)
|
||||
{
|
||||
Intent i = new Intent(this, typeof(SelectCurrentDbActivity));
|
||||
//don't show user notifications when an entry is opened.
|
||||
var task = new OpenSpecificEntryTask() { EntryUuid = entryUuid };
|
||||
SetTotpDependantActionsOnTask(task);
|
||||
task.ToIntent(i);
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using Android;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Preferences;
|
||||
using Android.Runtime;
|
||||
using keepass2android.services.AutofillBase;
|
||||
using keepass2android.services.AutofillBase.model;
|
||||
@@ -34,24 +35,29 @@ namespace keepass2android.services
|
||||
{
|
||||
}
|
||||
|
||||
protected override List<FilledAutofillFieldCollection<ViewNodeInputField>> GetSuggestedEntries(string query)
|
||||
protected override Dictionary<PwEntryOutput, FilledAutofillFieldCollection<ViewNodeInputField>> GetSuggestedEntries(string query)
|
||||
{
|
||||
if (!App.Kp2a.DatabaseIsUnlocked)
|
||||
return new List<FilledAutofillFieldCollection<ViewNodeInputField>>();
|
||||
return new Dictionary<PwEntryOutput, FilledAutofillFieldCollection<ViewNodeInputField>>();
|
||||
var foundEntries = (ShareUrlResults.GetSearchResultsForUrl(query)?.Entries ?? new PwObjectList<PwEntry>())
|
||||
.Select(e => new PwEntryOutput(e, App.Kp2a.FindDatabaseForElement(e)))
|
||||
.ToList();
|
||||
|
||||
if (App.Kp2a.LastOpenedEntry?.SearchUrl == query)
|
||||
{
|
||||
foundEntries.Clear();
|
||||
foundEntries.Add(App.Kp2a.LastOpenedEntry);
|
||||
foundEntries.Add(App.Kp2a.LastOpenedEntry?.Entry);
|
||||
}
|
||||
|
||||
//it seems like at least with Firefox we can have at most 3 datasets. Reserve space for the disable/enable dataset and the "fill with KP2A" which allows to select another item
|
||||
//so take only 1:
|
||||
return foundEntries.Take(1).Select(e => ChooseForAutofillActivity.GetFilledAutofillFieldCollectionFromEntry(e, this))
|
||||
.ToList();
|
||||
int numDisableDatasets = 0;
|
||||
if (!PreferenceManager.GetDefaultSharedPreferences(this)
|
||||
.GetBoolean(GetString(Resource.String.NoAutofillDisabling_key), false))
|
||||
numDisableDatasets = 1;
|
||||
|
||||
//it seems like at least with Firefox we can have at most 3 datasets. Reserve space for the disable dataset and the "fill with KP2A" which allows to select another item
|
||||
return foundEntries.Take(2-numDisableDatasets)
|
||||
.Select(e => new PwEntryOutput(e, App.Kp2a.FindDatabaseForElement(e)))
|
||||
.ToDictionary(e => e,
|
||||
e => ChooseForAutofillActivity.GetFilledAutofillFieldCollectionFromEntry(e, this));
|
||||
}
|
||||
|
||||
protected override void HandleSaveRequest(StructureParser parser, StructureParser.AutofillTargetId query)
|
||||
|
||||
@@ -9,6 +9,7 @@ using Android.Views;
|
||||
using Android.Widget;
|
||||
using keepass2android.services.AutofillBase;
|
||||
using keepass2android.services.Kp2aAutofill;
|
||||
using KeePassLib;
|
||||
|
||||
namespace keepass2android.services
|
||||
{
|
||||
@@ -29,16 +30,14 @@ namespace keepass2android.services
|
||||
return PendingIntent.GetActivity(context, _pendingIntentRequestCode++, intent, Util.AddMutabilityFlag(PendingIntentFlags.CancelCurrent, PendingIntentFlags.Mutable));
|
||||
}
|
||||
|
||||
public PendingIntent GetAuthPendingIntentForWarning(Context context, string query, string queryDomain, string queryPackage,
|
||||
public PendingIntent GetAuthPendingIntentForWarning(Context context,PwUuid entryUuid,
|
||||
AutofillServiceBase.DisplayWarning warning)
|
||||
{
|
||||
Intent intent = new Intent(context, typeof(ChooseForAutofillActivity));
|
||||
intent.PutExtra(ChooseForAutofillActivityBase.ExtraQueryString, query);
|
||||
intent.PutExtra(ChooseForAutofillActivityBase.ExtraQueryDomainString, queryDomain);
|
||||
intent.PutExtra(ChooseForAutofillActivityBase.ExtraQueryPackageString, queryPackage);
|
||||
intent.PutExtra(ChooseForAutofillActivityBase.ExtraUuidString, entryUuid.ToHexString());
|
||||
intent.PutExtra(ChooseForAutofillActivityBase.ExtraDisplayWarning, (int)warning);
|
||||
intent.PutExtra(ChooseForAutofillActivityBase.ExtraUseLastOpenedEntry, true);
|
||||
return PendingIntent.GetActivity(context, _pendingIntentRequestCode++, intent, Util.AddMutabilityFlag(PendingIntentFlags.CancelCurrent, PendingIntentFlags.Immutable));
|
||||
return PendingIntent.GetActivity(context, _pendingIntentRequestCode++, intent, Util.AddMutabilityFlag(PendingIntentFlags.CancelCurrent, PendingIntentFlags.Mutable));
|
||||
}
|
||||
|
||||
public PendingIntent GetDisablePendingIntentForResponse(Context context, string query,
|
||||
|
||||
Reference in New Issue
Block a user