mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Add Read support for TOTP Tokens
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -38,6 +38,13 @@ proguard/
|
|||||||
# Android Studio captures folder
|
# Android Studio captures folder
|
||||||
captures/
|
captures/
|
||||||
|
|
||||||
|
# Eclipse/VS Code
|
||||||
|
.project
|
||||||
|
.settings/*
|
||||||
|
*/.project
|
||||||
|
*/.classpath
|
||||||
|
*/.settings/*
|
||||||
|
|
||||||
# Intellij
|
# Intellij
|
||||||
*.iml
|
*.iml
|
||||||
.idea/workspace.xml
|
.idea/workspace.xml
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ vhschlenker
|
|||||||
bumper314 - Samsung multiwindow support
|
bumper314 - Samsung multiwindow support
|
||||||
Hans Cappelle - fingerprint sensor integration
|
Hans Cappelle - fingerprint sensor integration
|
||||||
Jeremy Jamet - Keepass DX Material Design - Patches
|
Jeremy Jamet - Keepass DX Material Design - Patches
|
||||||
|
somkun - TOTP support
|
||||||
|
|
||||||
Translations:
|
Translations:
|
||||||
Diego Pierotto - Italian
|
Diego Pierotto - Italian
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ dependencies {
|
|||||||
annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion"
|
annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion"
|
||||||
// Apache Commons Collections
|
// Apache Commons Collections
|
||||||
implementation 'commons-collections:commons-collections:3.2.1'
|
implementation 'commons-collections:commons-collections:3.2.1'
|
||||||
|
// Apache Commons Codec
|
||||||
|
implementation 'commons-codec:commons-codec:1.11'
|
||||||
// Base64
|
// Base64
|
||||||
implementation 'biz.source_code:base64coder:2010-12-19'
|
implementation 'biz.source_code:base64coder:2010-12-19'
|
||||||
implementation 'com.google.code.gson:gson:2.8.4'
|
implementation 'com.google.code.gson:gson:2.8.4'
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
/*
|
/*
|
||||||
*
|
*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
* it under the terms of the GNU General Public License as published by
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* License, or (at your option) any later version.
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
*
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
* KeePass DX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* General Public License for more details.
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along with KeePass DX. If not,
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities;
|
package com.kunzisoft.keepass.activities;
|
||||||
@@ -37,7 +35,6 @@ import android.view.MenuItem;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.getkeepsafe.taptargetview.TapTarget;
|
import com.getkeepsafe.taptargetview.TapTarget;
|
||||||
import com.getkeepsafe.taptargetview.TapTargetView;
|
import com.getkeepsafe.taptargetview.TapTargetView;
|
||||||
import com.kunzisoft.keepass.R;
|
import com.kunzisoft.keepass.R;
|
||||||
@@ -54,36 +51,36 @@ import com.kunzisoft.keepass.notifications.NotificationField;
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil;
|
import com.kunzisoft.keepass.settings.PreferencesUtil;
|
||||||
import com.kunzisoft.keepass.settings.SettingsAutofillActivity;
|
import com.kunzisoft.keepass.settings.SettingsAutofillActivity;
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper;
|
import com.kunzisoft.keepass.timeout.ClipboardHelper;
|
||||||
|
import com.kunzisoft.keepass.totp.*;
|
||||||
import com.kunzisoft.keepass.utils.EmptyUtils;
|
import com.kunzisoft.keepass.utils.EmptyUtils;
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil;
|
import com.kunzisoft.keepass.utils.MenuUtil;
|
||||||
import com.kunzisoft.keepass.utils.Types;
|
import com.kunzisoft.keepass.utils.Types;
|
||||||
import com.kunzisoft.keepass.utils.Util;
|
import com.kunzisoft.keepass.utils.Util;
|
||||||
import com.kunzisoft.keepass.view.EntryContentsView;
|
import com.kunzisoft.keepass.view.EntryContentsView;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static com.kunzisoft.keepass.settings.PreferencesUtil.isClipboardNotificationsEnable;
|
import static com.kunzisoft.keepass.settings.PreferencesUtil.isClipboardNotificationsEnable;
|
||||||
import static com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields;
|
import static com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields;
|
||||||
|
|
||||||
public class EntryActivity extends LockingHideActivity {
|
public class EntryActivity extends LockingHideActivity {
|
||||||
private final static String TAG = EntryActivity.class.getName();
|
private final static String TAG = EntryActivity.class.getName();
|
||||||
|
|
||||||
public static final String KEY_ENTRY = "entry";
|
public static final String KEY_ENTRY = "entry";
|
||||||
|
|
||||||
private ImageView titleIconView;
|
private ImageView titleIconView;
|
||||||
private TextView titleView;
|
private TextView titleView;
|
||||||
private EntryContentsView entryContentsView;
|
private EntryContentsView entryContentsView;
|
||||||
private Toolbar toolbar;
|
private Toolbar toolbar;
|
||||||
|
|
||||||
protected PwEntry mEntry;
|
|
||||||
private boolean mShowPassword;
|
|
||||||
|
|
||||||
private ClipboardHelper clipboardHelper;
|
protected PwEntry mEntry;
|
||||||
private boolean firstLaunchOfActivity;
|
private boolean mShowPassword;
|
||||||
|
private TotpSettings mTotpSettings;
|
||||||
|
|
||||||
private int iconColor;
|
private ClipboardHelper clipboardHelper;
|
||||||
|
private boolean firstLaunchOfActivity;
|
||||||
|
|
||||||
|
private int iconColor;
|
||||||
|
|
||||||
public static void launch(Activity act, PwEntry pw, boolean readOnly) {
|
public static void launch(Activity act, PwEntry pw, boolean readOnly) {
|
||||||
if (LockingActivity.checkTimeIsAllowedOrFinish(act)) {
|
if (LockingActivity.checkTimeIsAllowedOrFinish(act)) {
|
||||||
@@ -94,60 +91,62 @@ public class EntryActivity extends LockingHideActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
setContentView(R.layout.entry_view);
|
setContentView(R.layout.entry_view);
|
||||||
|
|
||||||
toolbar = findViewById(R.id.toolbar);
|
toolbar = findViewById(R.id.toolbar);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
assert getSupportActionBar() != null;
|
assert getSupportActionBar() != null;
|
||||||
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
|
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||||
|
|
||||||
Database db = App.getDB();
|
Database db = App.getDB();
|
||||||
// Likely the app has been killed exit the activity
|
// Likely the app has been killed exit the activity
|
||||||
if ( ! db.getLoaded() ) {
|
if (!db.getLoaded()) {
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
readOnly = db.isReadOnly() || readOnly;
|
readOnly = db.isReadOnly() || readOnly;
|
||||||
|
|
||||||
mShowPassword = !PreferencesUtil.isPasswordMask(this);
|
mShowPassword = !PreferencesUtil.isPasswordMask(this);
|
||||||
|
|
||||||
// Get Entry from UUID
|
// Get Entry from UUID
|
||||||
Intent i = getIntent();
|
Intent i = getIntent();
|
||||||
UUID uuid = Types.bytestoUUID(i.getByteArrayExtra(KEY_ENTRY));
|
UUID uuid = Types.bytestoUUID(i.getByteArrayExtra(KEY_ENTRY));
|
||||||
mEntry = db.getPwDatabase().getEntryByUUIDId(uuid);
|
mEntry = db.getPwDatabase().getEntryByUUIDId(uuid);
|
||||||
if (mEntry == null) {
|
if (mEntry == null) {
|
||||||
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show();
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
mTotpSettings = new TotpSettings(mEntry);
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
int[] attrs = {R.attr.textColorInverse};
|
int[] attrs = {R.attr.textColorInverse};
|
||||||
TypedArray ta = getTheme().obtainStyledAttributes(attrs);
|
TypedArray ta = getTheme().obtainStyledAttributes(attrs);
|
||||||
iconColor = ta.getColor(0, Color.WHITE);
|
iconColor = ta.getColor(0, Color.WHITE);
|
||||||
|
|
||||||
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
||||||
invalidateOptionsMenu();
|
invalidateOptionsMenu();
|
||||||
|
|
||||||
// Update last access time.
|
// Update last access time.
|
||||||
mEntry.touch(false, false);
|
mEntry.touch(false, false);
|
||||||
|
|
||||||
// Get views
|
// Get views
|
||||||
titleIconView = findViewById(R.id.entry_icon);
|
titleIconView = findViewById(R.id.entry_icon);
|
||||||
titleView = findViewById(R.id.entry_title);
|
titleView = findViewById(R.id.entry_title);
|
||||||
entryContentsView = findViewById(R.id.entry_contents);
|
entryContentsView = findViewById(R.id.entry_contents);
|
||||||
entryContentsView.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this));
|
entryContentsView
|
||||||
|
.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this));
|
||||||
|
|
||||||
// Init the clipboard helper
|
// Init the clipboard helper
|
||||||
clipboardHelper = new ClipboardHelper(this);
|
clipboardHelper = new ClipboardHelper(this);
|
||||||
firstLaunchOfActivity = true;
|
firstLaunchOfActivity = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
@@ -160,76 +159,68 @@ public class EntryActivity extends LockingHideActivity {
|
|||||||
// Start to manage field reference to copy a value from ref
|
// Start to manage field reference to copy a value from ref
|
||||||
mEntry.startToManageFieldReferences(App.getDB().getPwDatabase());
|
mEntry.startToManageFieldReferences(App.getDB().getPwDatabase());
|
||||||
|
|
||||||
boolean containsUsernameToCopy =
|
boolean containsUsernameToCopy = mEntry.getUsername().length() > 0;
|
||||||
mEntry.getUsername().length() > 0;
|
boolean containsPasswordToCopy = (mEntry.getPassword().length() > 0
|
||||||
boolean containsPasswordToCopy =
|
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(this));
|
||||||
(mEntry.getPassword().length() > 0
|
boolean containsExtraFieldToCopy = (mEntry.allowExtraFields()
|
||||||
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(this));
|
&& ((mEntry.containsCustomFields() && mEntry.containsCustomFieldsNotProtected())
|
||||||
boolean containsExtraFieldToCopy =
|
|| (mEntry.containsCustomFields() && mEntry.containsCustomFieldsProtected()
|
||||||
(mEntry.allowExtraFields()
|
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(this))));
|
||||||
&& ((mEntry.containsCustomFields()
|
|
||||||
&& mEntry.containsCustomFieldsNotProtected())
|
|
||||||
|| (mEntry.containsCustomFields()
|
|
||||||
&& mEntry.containsCustomFieldsProtected()
|
|
||||||
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(this))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// If notifications enabled in settings
|
// If notifications enabled in settings
|
||||||
// Don't if application timeout
|
// Don't if application timeout
|
||||||
if (firstLaunchOfActivity && !App.isShutdown() && isClipboardNotificationsEnable(getApplicationContext())) {
|
if (firstLaunchOfActivity && !App.isShutdown()
|
||||||
if (containsUsernameToCopy
|
&& isClipboardNotificationsEnable(getApplicationContext())) {
|
||||||
|| containsPasswordToCopy
|
if (containsUsernameToCopy || containsPasswordToCopy || containsExtraFieldToCopy) {
|
||||||
|| containsExtraFieldToCopy
|
|
||||||
) {
|
|
||||||
// username already copied, waiting for user's action before copy password.
|
// username already copied, waiting for user's action before copy password.
|
||||||
Intent intent = new Intent(this, NotificationCopyingService.class);
|
Intent intent = new Intent(this, NotificationCopyingService.class);
|
||||||
intent.setAction(NotificationCopyingService.ACTION_NEW_NOTIFICATION);
|
intent.setAction(NotificationCopyingService.ACTION_NEW_NOTIFICATION);
|
||||||
if (mEntry.getTitle() != null)
|
if (mEntry.getTitle() != null)
|
||||||
intent.putExtra(NotificationCopyingService.EXTRA_ENTRY_TITLE, mEntry.getTitle());
|
intent.putExtra(NotificationCopyingService.EXTRA_ENTRY_TITLE,
|
||||||
|
mEntry.getTitle());
|
||||||
// Construct notification fields
|
// Construct notification fields
|
||||||
ArrayList<NotificationField> notificationFields = new ArrayList<>();
|
ArrayList<NotificationField> notificationFields = new ArrayList<>();
|
||||||
// Add username if exists to notifications
|
// Add username if exists to notifications
|
||||||
if (containsUsernameToCopy)
|
if (containsUsernameToCopy)
|
||||||
notificationFields.add(
|
notificationFields.add(
|
||||||
new NotificationField(
|
new NotificationField(NotificationField.NotificationFieldId.USERNAME,
|
||||||
NotificationField.NotificationFieldId.USERNAME,
|
mEntry.getUsername(), getResources()));
|
||||||
mEntry.getUsername(),
|
|
||||||
getResources()));
|
|
||||||
// Add password to notifications
|
// Add password to notifications
|
||||||
if (containsPasswordToCopy) {
|
if (containsPasswordToCopy) {
|
||||||
notificationFields.add(
|
notificationFields.add(
|
||||||
new NotificationField(
|
new NotificationField(NotificationField.NotificationFieldId.PASSWORD,
|
||||||
NotificationField.NotificationFieldId.PASSWORD,
|
mEntry.getPassword(), getResources()));
|
||||||
mEntry.getPassword(),
|
|
||||||
getResources()));
|
|
||||||
}
|
}
|
||||||
// Add extra fields
|
// Add extra fields
|
||||||
if (containsExtraFieldToCopy) {
|
if (containsExtraFieldToCopy) {
|
||||||
try {
|
try {
|
||||||
mEntry.getFields().doActionToAllCustomProtectedField(new ExtraFields.ActionProtected() {
|
mEntry.getFields().doActionToAllCustomProtectedField(
|
||||||
private int anonymousFieldNumber = 0;
|
new ExtraFields.ActionProtected() {
|
||||||
@Override
|
private int anonymousFieldNumber = 0;
|
||||||
public void doAction(String key, ProtectedString value) {
|
|
||||||
//If value is not protected or allowed
|
@Override
|
||||||
if (!value.isProtected() || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this)) {
|
public void doAction(String key, ProtectedString value) {
|
||||||
notificationFields.add(
|
// If value is not protected or allowed
|
||||||
new NotificationField(
|
if (!value.isProtected() || PreferencesUtil
|
||||||
NotificationField.NotificationFieldId.getAnonymousFieldId()[anonymousFieldNumber],
|
.allowCopyPasswordAndProtectedFields(
|
||||||
value.toString(),
|
EntryActivity.this)) {
|
||||||
key,
|
notificationFields.add(new NotificationField(
|
||||||
getResources()));
|
NotificationField.NotificationFieldId
|
||||||
anonymousFieldNumber++;
|
.getAnonymousFieldId()[anonymousFieldNumber],
|
||||||
}
|
value.toString(), key, getResources()));
|
||||||
}
|
anonymousFieldNumber++;
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (ArrayIndexOutOfBoundsException e) {
|
} catch (ArrayIndexOutOfBoundsException e) {
|
||||||
Log.w(TAG, "Only " + NotificationField.NotificationFieldId.getAnonymousFieldId().length +
|
Log.w(TAG, "Only "
|
||||||
" anonymous notifications are available");
|
+ NotificationField.NotificationFieldId.getAnonymousFieldId().length
|
||||||
|
+ " anonymous notifications are available");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Add notifications
|
// Add notifications
|
||||||
intent.putParcelableArrayListExtra(NotificationCopyingService.EXTRA_FIELDS, notificationFields);
|
intent.putParcelableArrayListExtra(NotificationCopyingService.EXTRA_FIELDS,
|
||||||
|
notificationFields);
|
||||||
|
|
||||||
startService(intent);
|
startService(intent);
|
||||||
}
|
}
|
||||||
@@ -239,27 +230,28 @@ public class EntryActivity extends LockingHideActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check and display learning views
|
* Check and display learning views Displays the explanation for copying a field and editing an
|
||||||
* Displays the explanation for copying a field and editing an entry
|
* entry
|
||||||
*/
|
*/
|
||||||
private void checkAndPerformedEducation(Menu menu) {
|
private void checkAndPerformedEducation(Menu menu) {
|
||||||
if (PreferencesUtil.isEducationScreensEnabled(this)) {
|
if (PreferencesUtil.isEducationScreensEnabled(this)) {
|
||||||
|
|
||||||
if (entryContentsView != null && entryContentsView.isUserNamePresent()
|
if (entryContentsView != null && entryContentsView.isUserNamePresent()
|
||||||
&& !PreferencesUtil.isEducationCopyUsernamePerformed(this)) {
|
&& !PreferencesUtil.isEducationCopyUsernamePerformed(this)) {
|
||||||
TapTargetView.showFor(this,
|
TapTargetView.showFor(
|
||||||
TapTarget.forView(findViewById(R.id.entry_user_name_action_image),
|
this,
|
||||||
getString(R.string.education_field_copy_title),
|
TapTarget
|
||||||
getString(R.string.education_field_copy_summary))
|
.forView(findViewById(R.id.entry_user_name_action_image),
|
||||||
.textColorInt(Color.WHITE)
|
getString(R.string.education_field_copy_title),
|
||||||
.tintTarget(false)
|
getString(R.string.education_field_copy_summary))
|
||||||
.cancelable(true),
|
.textColorInt(Color.WHITE).tintTarget(false).cancelable(true),
|
||||||
new TapTargetView.Listener() {
|
new TapTargetView.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTargetClick(TapTargetView view) {
|
public void onTargetClick(TapTargetView view) {
|
||||||
super.onTargetClick(view);
|
super.onTargetClick(view);
|
||||||
clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(),
|
clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(),
|
||||||
getString(R.string.copy_field, getString(R.string.entry_user_name)));
|
getString(R.string.copy_field,
|
||||||
|
getString(R.string.entry_user_name)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -267,40 +259,43 @@ public class EntryActivity extends LockingHideActivity {
|
|||||||
super.onOuterCircleClick(view);
|
super.onOuterCircleClick(view);
|
||||||
view.dismiss(false);
|
view.dismiss(false);
|
||||||
// Launch autofill settings
|
// Launch autofill settings
|
||||||
startActivity(new Intent(EntryActivity.this, SettingsAutofillActivity.class));
|
startActivity(new Intent(EntryActivity.this,
|
||||||
|
SettingsAutofillActivity.class));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
PreferencesUtil.saveEducationPreference(this,
|
PreferencesUtil.saveEducationPreference(this, R.string.education_copy_username_key);
|
||||||
R.string.education_copy_username_key);
|
|
||||||
|
|
||||||
} else if (!PreferencesUtil.isEducationEntryEditPerformed(this)) {
|
} else if (!PreferencesUtil.isEducationEntryEditPerformed(this)) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
TapTargetView.showFor(this,
|
TapTargetView
|
||||||
TapTarget.forToolbarMenuItem(toolbar, R.id.menu_edit,
|
.showFor(this,
|
||||||
getString(R.string.education_entry_edit_title),
|
TapTarget
|
||||||
getString(R.string.education_entry_edit_summary))
|
.forToolbarMenuItem(toolbar, R.id.menu_edit,
|
||||||
.textColorInt(Color.WHITE)
|
getString(R.string.education_entry_edit_title),
|
||||||
.tintTarget(true)
|
getString(
|
||||||
.cancelable(true),
|
R.string.education_entry_edit_summary))
|
||||||
new TapTargetView.Listener() {
|
.textColorInt(Color.WHITE).tintTarget(true)
|
||||||
@Override
|
.cancelable(true),
|
||||||
public void onTargetClick(TapTargetView view) {
|
new TapTargetView.Listener() {
|
||||||
super.onTargetClick(view);
|
@Override
|
||||||
MenuItem editItem = menu.findItem(R.id.menu_edit);
|
public void onTargetClick(TapTargetView view) {
|
||||||
onOptionsItemSelected(editItem);
|
super.onTargetClick(view);
|
||||||
}
|
MenuItem editItem = menu.findItem(R.id.menu_edit);
|
||||||
|
onOptionsItemSelected(editItem);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOuterCircleClick(TapTargetView view) {
|
public void onOuterCircleClick(TapTargetView view) {
|
||||||
super.onOuterCircleClick(view);
|
super.onOuterCircleClick(view);
|
||||||
view.dismiss(false);
|
view.dismiss(false);
|
||||||
// Open Keepass doc to create field references
|
// Open Keepass doc to create field references
|
||||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW,
|
Intent browserIntent = new Intent(Intent.ACTION_VIEW,
|
||||||
Uri.parse(getString(R.string.field_references_url)));
|
Uri.parse(getString(
|
||||||
startActivity(browserIntent);
|
R.string.field_references_url)));
|
||||||
}
|
startActivity(browserIntent);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
PreferencesUtil.saveEducationPreference(this,
|
PreferencesUtil.saveEducationPreference(this,
|
||||||
R.string.education_entry_edit_key);
|
R.string.education_entry_edit_key);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -311,50 +306,49 @@ public class EntryActivity extends LockingHideActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void fillData() {
|
protected void fillData() {
|
||||||
Database db = App.getDB();
|
Database db = App.getDB();
|
||||||
PwDatabase pm = db.getPwDatabase();
|
PwDatabase pm = db.getPwDatabase();
|
||||||
|
|
||||||
mEntry.startToManageFieldReferences(pm);
|
mEntry.startToManageFieldReferences(pm);
|
||||||
|
|
||||||
// Assign title icon
|
// Assign title icon
|
||||||
db.getDrawFactory().assignDatabaseIconTo(this, titleIconView, mEntry.getIcon(), iconColor);
|
db.getDrawFactory().assignDatabaseIconTo(this, titleIconView, mEntry.getIcon(), iconColor);
|
||||||
|
|
||||||
// Assign title text
|
// Assign title text
|
||||||
titleView.setText(mEntry.getVisualTitle());
|
titleView.setText(mEntry.getVisualTitle());
|
||||||
|
|
||||||
// Assign basic fields
|
// Assign basic fields
|
||||||
entryContentsView.assignUserName(mEntry.getUsername());
|
entryContentsView.assignUserName(mEntry.getUsername());
|
||||||
entryContentsView.assignUserNameCopyListener(view ->
|
entryContentsView.assignUserNameCopyListener(
|
||||||
clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(),
|
view -> clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(),
|
||||||
getString(R.string.copy_field, getString(R.string.entry_user_name)))
|
getString(R.string.copy_field, getString(R.string.entry_user_name))));
|
||||||
);
|
|
||||||
|
|
||||||
boolean allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this);
|
boolean allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this);
|
||||||
entryContentsView.assignPassword(mEntry.getPassword(), allowCopyPassword);
|
entryContentsView.assignPassword(mEntry.getPassword(), allowCopyPassword);
|
||||||
if (allowCopyPassword) {
|
if (allowCopyPassword) {
|
||||||
entryContentsView.assignPasswordCopyListener(view ->
|
entryContentsView.assignPasswordCopyListener(
|
||||||
clipboardHelper.timeoutCopyToClipboard(mEntry.getPassword(),
|
view -> clipboardHelper.timeoutCopyToClipboard(mEntry.getPassword(),
|
||||||
getString(R.string.copy_field, getString(R.string.entry_password)))
|
getString(R.string.copy_field, getString(R.string.entry_password))));
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
// If dialog not already shown
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) {
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) {
|
||||||
entryContentsView.assignPasswordCopyListener(v -> {
|
entryContentsView.assignPasswordCopyListener(v -> {
|
||||||
String message = getString(R.string.allow_copy_password_warning) +
|
String message = getString(R.string.allow_copy_password_warning) + "\n\n"
|
||||||
"\n\n" +
|
+ getString(R.string.clipboard_warning);
|
||||||
getString(R.string.clipboard_warning);
|
|
||||||
AlertDialog warningDialog = new AlertDialog.Builder(EntryActivity.this)
|
AlertDialog warningDialog = new AlertDialog.Builder(EntryActivity.this)
|
||||||
.setMessage(message).create();
|
.setMessage(message).create();
|
||||||
warningDialog.setButton(AlertDialog.BUTTON1, getText(android.R.string.ok),
|
warningDialog.setButton(AlertDialog.BUTTON1, getText(android.R.string.ok),
|
||||||
(dialog, which) -> {
|
(dialog, which) -> {
|
||||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(EntryActivity.this, true);
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(
|
||||||
|
EntryActivity.this, true);
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
fillData();
|
fillData();
|
||||||
});
|
});
|
||||||
warningDialog.setButton(AlertDialog.BUTTON2, getText(android.R.string.cancel),
|
warningDialog.setButton(AlertDialog.BUTTON2, getText(android.R.string.cancel),
|
||||||
(dialog, which) -> {
|
(dialog, which) -> {
|
||||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(EntryActivity.this, false);
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(
|
||||||
|
EntryActivity.this, false);
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
fillData();
|
fillData();
|
||||||
});
|
});
|
||||||
@@ -367,84 +361,87 @@ public class EntryActivity extends LockingHideActivity {
|
|||||||
|
|
||||||
entryContentsView.assignURL(mEntry.getUrl());
|
entryContentsView.assignURL(mEntry.getUrl());
|
||||||
|
|
||||||
|
entryContentsView.assignTotp(mTotpSettings,
|
||||||
|
view -> clipboardHelper.timeoutCopyToClipboard(mTotpSettings.getToken(),
|
||||||
|
getString(R.string.copy_field, getString(R.string.entry_totp))));
|
||||||
|
|
||||||
entryContentsView.setHiddenPasswordStyle(!mShowPassword);
|
entryContentsView.setHiddenPasswordStyle(!mShowPassword);
|
||||||
entryContentsView.assignComment(mEntry.getNotes());
|
entryContentsView.assignComment(mEntry.getNotes());
|
||||||
|
|
||||||
// Assign custom fields
|
// Assign custom fields
|
||||||
if (mEntry.allowExtraFields()) {
|
if (mEntry.allowExtraFields()) {
|
||||||
entryContentsView.clearExtraFields();
|
entryContentsView.clearExtraFields();
|
||||||
|
|
||||||
mEntry.getFields().doActionToAllCustomProtectedField((label, value) -> {
|
mEntry.getFields().doActionToAllCustomProtectedField((label, value) -> {
|
||||||
boolean showAction = (!value.isProtected() || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this));
|
boolean showAction = (!value.isProtected()
|
||||||
entryContentsView.addExtraField(label, value, showAction, view ->
|
|| PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this));
|
||||||
clipboardHelper.timeoutCopyToClipboard(
|
entryContentsView.addExtraField(label, value, showAction,
|
||||||
value.toString(),
|
view -> clipboardHelper.timeoutCopyToClipboard(value.toString(),
|
||||||
getString(R.string.copy_field, label)
|
getString(R.string.copy_field, label)));
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign dates
|
// Assign dates
|
||||||
entryContentsView.assignCreationDate(mEntry.getCreationTime().getDate());
|
entryContentsView.assignCreationDate(mEntry.getCreationTime().getDate());
|
||||||
entryContentsView.assignModificationDate(mEntry.getLastModificationTime().getDate());
|
entryContentsView.assignModificationDate(mEntry.getLastModificationTime().getDate());
|
||||||
entryContentsView.assignLastAccessDate(mEntry.getLastAccessTime().getDate());
|
entryContentsView.assignLastAccessDate(mEntry.getLastAccessTime().getDate());
|
||||||
Date expires = mEntry.getExpiryTime().getDate();
|
Date expires = mEntry.getExpiryTime().getDate();
|
||||||
if ( mEntry.isExpires() ) {
|
if (mEntry.isExpires()) {
|
||||||
entryContentsView.assignExpiresDate(expires);
|
entryContentsView.assignExpiresDate(expires);
|
||||||
} else {
|
} else {
|
||||||
entryContentsView.assignExpiresDate(getString(R.string.never));
|
entryContentsView.assignExpiresDate(getString(R.string.never));
|
||||||
}
|
}
|
||||||
|
|
||||||
mEntry.stopToManageFieldReferences();
|
mEntry.stopToManageFieldReferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE:
|
case EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE:
|
||||||
fillData();
|
fillData();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void changeShowPasswordIcon(MenuItem togglePassword) {
|
private void changeShowPasswordIcon(MenuItem togglePassword) {
|
||||||
if ( mShowPassword ) {
|
if (mShowPassword) {
|
||||||
togglePassword.setTitle(R.string.menu_hide_password);
|
togglePassword.setTitle(R.string.menu_hide_password);
|
||||||
togglePassword.setIcon(R.drawable.ic_visibility_off_white_24dp);
|
togglePassword.setIcon(R.drawable.ic_visibility_off_white_24dp);
|
||||||
} else {
|
} else {
|
||||||
togglePassword.setTitle(R.string.menu_showpass);
|
togglePassword.setTitle(R.string.menu_showpass);
|
||||||
togglePassword.setIcon(R.drawable.ic_visibility_white_24dp);
|
togglePassword.setIcon(R.drawable.ic_visibility_white_24dp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
super.onCreateOptionsMenu(menu);
|
super.onCreateOptionsMenu(menu);
|
||||||
|
|
||||||
MenuInflater inflater = getMenuInflater();
|
MenuInflater inflater = getMenuInflater();
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu);
|
MenuUtil.contributionMenuInflater(inflater, menu);
|
||||||
inflater.inflate(R.menu.entry, menu);
|
inflater.inflate(R.menu.entry, menu);
|
||||||
inflater.inflate(R.menu.database_lock, menu);
|
inflater.inflate(R.menu.database_lock, menu);
|
||||||
|
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
MenuItem edit = menu.findItem(R.id.menu_edit);
|
MenuItem edit = menu.findItem(R.id.menu_edit);
|
||||||
if (edit != null)
|
if (edit != null)
|
||||||
edit.setVisible(false);
|
edit.setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem togglePassword = menu.findItem(R.id.menu_toggle_pass);
|
MenuItem togglePassword = menu.findItem(R.id.menu_toggle_pass);
|
||||||
if (entryContentsView != null && togglePassword != null) {
|
if (entryContentsView != null && togglePassword != null) {
|
||||||
if (entryContentsView.isPasswordPresent() || entryContentsView.atLeastOneFieldProtectedPresent()) {
|
if (entryContentsView.isPasswordPresent()
|
||||||
|
|| entryContentsView.atLeastOneFieldProtectedPresent()) {
|
||||||
changeShowPasswordIcon(togglePassword);
|
changeShowPasswordIcon(togglePassword);
|
||||||
} else {
|
} else {
|
||||||
togglePassword.setVisible(false);
|
togglePassword.setVisible(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem gotoUrl = menu.findItem(R.id.menu_goto_url);
|
MenuItem gotoUrl = menu.findItem(R.id.menu_goto_url);
|
||||||
if (gotoUrl != null) {
|
if (gotoUrl != null) {
|
||||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
||||||
// so mEntry may not be set
|
// so mEntry may not be set
|
||||||
if (mEntry == null) {
|
if (mEntry == null) {
|
||||||
@@ -460,13 +457,13 @@ public class EntryActivity extends LockingHideActivity {
|
|||||||
|
|
||||||
// Show education views
|
// Show education views
|
||||||
new Handler().post(() -> checkAndPerformedEducation(menu));
|
new Handler().post(() -> checkAndPerformedEducation(menu));
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
return true;
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
}
|
||||||
switch ( item.getItemId() ) {
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
case R.id.menu_contribute:
|
case R.id.menu_contribute:
|
||||||
return MenuUtil.onContributionItemSelected(this);
|
return MenuUtil.onContributionItemSelected(this);
|
||||||
|
|
||||||
@@ -479,13 +476,13 @@ public class EntryActivity extends LockingHideActivity {
|
|||||||
case R.id.menu_edit:
|
case R.id.menu_edit:
|
||||||
EntryEditActivity.launch(EntryActivity.this, mEntry);
|
EntryEditActivity.launch(EntryActivity.this, mEntry);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case R.id.menu_goto_url:
|
case R.id.menu_goto_url:
|
||||||
String url;
|
String url;
|
||||||
url = mEntry.getUrl();
|
url = mEntry.getUrl();
|
||||||
|
|
||||||
// Default http:// if no protocol specified
|
// Default http:// if no protocol specified
|
||||||
if ( ! url.contains("://") ) {
|
if (!url.contains("://")) {
|
||||||
url = "http://" + url;
|
url = "http://" + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,28 +492,27 @@ public class EntryActivity extends LockingHideActivity {
|
|||||||
Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case R.id.menu_lock:
|
case R.id.menu_lock:
|
||||||
lockAndExit();
|
lockAndExit();
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case android.R.id.home :
|
case android.R.id.home:
|
||||||
finish(); // close this activity and return to preview activity (if there is any)
|
finish(); // close this activity and return to preview activity (if there is any)
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void finish() {
|
public void finish() {
|
||||||
// Transit data in previous Activity after an update
|
// Transit data in previous Activity after an update
|
||||||
/*
|
/*
|
||||||
TODO Slowdown when add entry as result
|
* TODO Slowdown when add entry as result Intent intent = new Intent();
|
||||||
Intent intent = new Intent();
|
* intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
|
||||||
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
|
* setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
|
||||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
|
*/
|
||||||
*/
|
|
||||||
super.finish();
|
super.finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
111
app/src/main/java/com/kunzisoft/keepass/totp/TotpGenerator.java
Normal file
111
app/src/main/java/com/kunzisoft/keepass/totp/TotpGenerator.java
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePass DX.
|
||||||
|
*
|
||||||
|
* KeePass DX is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePass DX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||||
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with KeePass DX. If not,
|
||||||
|
* see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* This code is based on andOTP code
|
||||||
|
* https://github.com/andOTP/andOTP/blob/master/app/src/main/java/org/shadowice/flocke/andotp/
|
||||||
|
* Utilities/TokenCalculator.java
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.totp;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.util.Patterns;
|
||||||
|
|
||||||
|
public final class TotpGenerator {
|
||||||
|
|
||||||
|
private static final char[] STEAM_CHARS =
|
||||||
|
new char[] {'2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J',
|
||||||
|
'K', 'M', 'N', 'P', 'Q', 'R', 'T', 'V', 'W', 'X', 'Y'};
|
||||||
|
private static final String ALGORITHM = "HmacSHA1";
|
||||||
|
|
||||||
|
private static byte[] generateHash(byte[] key, byte[] data)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||||
|
|
||||||
|
Mac mac = Mac.getInstance(ALGORITHM);
|
||||||
|
mac.init(new SecretKeySpec(key, ALGORITHM));
|
||||||
|
|
||||||
|
return mac.doFinal(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int TOTP_RFC6238(byte[] secret, int period, long time, int digits) {
|
||||||
|
int fullToken = TOTP(secret, period, time);
|
||||||
|
int div = (int) Math.pow(10, digits);
|
||||||
|
|
||||||
|
return fullToken % div;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String TOTP_RFC6238(byte[] secret, int period, int digits) {
|
||||||
|
int token = TOTP_RFC6238(secret, period, System.currentTimeMillis() / 1000, digits);
|
||||||
|
|
||||||
|
return String.format("%0" + digits + "d", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String TOTP_Steam(byte[] secret, int period, int digits) {
|
||||||
|
int fullToken = TOTP(secret, period, System.currentTimeMillis() / 1000);
|
||||||
|
|
||||||
|
StringBuilder tokenBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
for (int i = 0; i < digits; i++) {
|
||||||
|
tokenBuilder.append(STEAM_CHARS[fullToken % STEAM_CHARS.length]);
|
||||||
|
fullToken /= STEAM_CHARS.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String HOTP(byte[] secret, long counter, int digits) {
|
||||||
|
int fullToken = HOTP(secret, counter);
|
||||||
|
int div = (int) Math.pow(10, digits);
|
||||||
|
|
||||||
|
return String.format("%0" + digits + "d", fullToken % div);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int TOTP(byte[] key, int period, long time) {
|
||||||
|
return HOTP(key, time / period);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int HOTP(byte[] key, long counter) {
|
||||||
|
int r = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] data = ByteBuffer.allocate(8).putLong(counter).array();
|
||||||
|
byte[] hash = generateHash(key, data);
|
||||||
|
|
||||||
|
int offset = hash[hash.length - 1] & 0xF;
|
||||||
|
|
||||||
|
int binary = (hash[offset] & 0x7F) << 0x18;
|
||||||
|
binary |= (hash[offset + 1] & 0xFF) << 0x10;
|
||||||
|
binary |= (hash[offset + 2] & 0xFF) << 0x08;
|
||||||
|
binary |= (hash[offset + 3] & 0xFF);
|
||||||
|
|
||||||
|
r = binary;
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
221
app/src/main/java/com/kunzisoft/keepass/totp/TotpSettings.java
Normal file
221
app/src/main/java/com/kunzisoft/keepass/totp/TotpSettings.java
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePass DX.
|
||||||
|
*
|
||||||
|
* KeePass DX is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePass DX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||||
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with KeePass DX. If not,
|
||||||
|
* see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* This code is based on KeePassXC code
|
||||||
|
* https://github.com/keepassxreboot/keepassxc/blob/master/src/totp/totp.cpp
|
||||||
|
* https://github.com/keepassxreboot/keepassxc/blob/master/src/core/Entry.cpp
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.totp;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base32;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.util.Patterns;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import com.kunzisoft.keepass.database.security.ProtectedString;
|
||||||
|
import com.kunzisoft.keepass.database.PwEntry;
|
||||||
|
|
||||||
|
public class TotpSettings {
|
||||||
|
|
||||||
|
private enum EntryType {
|
||||||
|
None, OTP, SeedAndSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum TokenType {
|
||||||
|
Default, Steam
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int DEFAULT_STEP = 30;
|
||||||
|
private static final int DEFAULT_DIGITS = 6;
|
||||||
|
private static final int STEAM_DIGITS = 5;
|
||||||
|
|
||||||
|
// Logical breakdown of key=value regex. the final string is as follows:
|
||||||
|
// [^&=\s]+=[^&=\s]+(&[^&=\s]+=[^&=\s]+)*
|
||||||
|
private static final String validKeyValue = "[^&=\\s]+";
|
||||||
|
private static final String validKeyValuePair = validKeyValue + "=" + validKeyValue;
|
||||||
|
private static final String validKeyValueRegex =
|
||||||
|
validKeyValuePair + "&(" + validKeyValuePair + ")*";
|
||||||
|
|
||||||
|
private static final String OTP_FIELD = "otp";
|
||||||
|
private static final String SEED_FIELD = "TOTP Seed";
|
||||||
|
private static final String SETTING_FIELD = "TOTP Settings";
|
||||||
|
|
||||||
|
private PwEntry entry;
|
||||||
|
private String seed;
|
||||||
|
private byte[] secret;
|
||||||
|
private int step;
|
||||||
|
private int digits;
|
||||||
|
private EntryType entryType;
|
||||||
|
private TokenType tokenType;
|
||||||
|
|
||||||
|
public TotpSettings(PwEntry entry) {
|
||||||
|
this.entry = entry;
|
||||||
|
if (parseOtp() || parseSeedAndSettings()) {
|
||||||
|
secret = new Base32().decode(seed.getBytes());
|
||||||
|
} else {
|
||||||
|
entryType = EntryType.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSettings(String seed, int digits, int step) {
|
||||||
|
// TODO: Implement a way to set TOTP from device
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConfigured() {
|
||||||
|
return entryType != EntryType.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getToken() {
|
||||||
|
if (entryType == EntryType.None) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
switch (tokenType) {
|
||||||
|
case Steam:
|
||||||
|
return TotpGenerator.TOTP_Steam(secret, step, digits);
|
||||||
|
default:
|
||||||
|
return TotpGenerator.TOTP_RFC6238(secret, step, digits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSecondsRemaining() {
|
||||||
|
return step - (int) ((System.currentTimeMillis() / 1000) % step);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldRefreshToken() {
|
||||||
|
return getSecondsRemaining() == step;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean parseSeedAndSettings() {
|
||||||
|
String seedField = getField(SEED_FIELD);
|
||||||
|
String settingsField = getField(SETTING_FIELD);
|
||||||
|
if (seedField == null || settingsField == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regex match, sync with TotpGenerator.shortNameToEncoder
|
||||||
|
Pattern pattern = Pattern.compile("(\\d+);((?:\\d+)|S)");
|
||||||
|
Matcher matcher = pattern.matcher(settingsField);
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
// malformed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
step = toInt(matcher.group(1));
|
||||||
|
|
||||||
|
String encodingType = matcher.group(2);
|
||||||
|
digits = getDigitsForType(encodingType);
|
||||||
|
|
||||||
|
seed = seedField;
|
||||||
|
entryType = EntryType.SeedAndSettings;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean parseOtp() {
|
||||||
|
String key = getField(OTP_FIELD);
|
||||||
|
if (key == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri url = null;
|
||||||
|
if (isValidUrl(key)) {
|
||||||
|
url = Uri.parse(key);
|
||||||
|
}
|
||||||
|
boolean useEncoder = false;
|
||||||
|
|
||||||
|
if (url != null && url.getScheme().equals("otpauth")) {
|
||||||
|
// Default OTP url format
|
||||||
|
|
||||||
|
seed = url.getQueryParameter("secret");
|
||||||
|
digits = toInt(url.getQueryParameter("digits"));
|
||||||
|
step = toInt(url.getQueryParameter("period"));
|
||||||
|
|
||||||
|
String encName = url.getQueryParameter("encoder");
|
||||||
|
digits = getDigitsForType(encName);
|
||||||
|
} else if (Pattern.matches(validKeyValueRegex, key)) {
|
||||||
|
// KeeOtp string format
|
||||||
|
HashMap<String, String> query = breakDownKeyValuePairs(key);
|
||||||
|
|
||||||
|
seed = query.get("key");
|
||||||
|
digits = toInt(query.get("size"));
|
||||||
|
step = toInt(query.get("step"));
|
||||||
|
} else {
|
||||||
|
// Malformed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digits == 0) {
|
||||||
|
digits = DEFAULT_DIGITS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step <= 0 || step > 60) {
|
||||||
|
step = DEFAULT_STEP;
|
||||||
|
}
|
||||||
|
|
||||||
|
entryType = EntryType.OTP;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getField(String id) {
|
||||||
|
ProtectedString field = entry.getFields().getListOfAllFields().get(id);
|
||||||
|
if (field != null) {
|
||||||
|
return field.toString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidUrl(String url) {
|
||||||
|
return Patterns.WEB_URL.matcher(url).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int toInt(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(value);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashMap<String, String> breakDownKeyValuePairs(String pairs) {
|
||||||
|
String[] elements = pairs.split("&");
|
||||||
|
HashMap<String, String> output = new HashMap<String, String>();
|
||||||
|
for (String element : elements) {
|
||||||
|
String[] pair = element.split("=");
|
||||||
|
output.put(pair[0], pair[1]);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getDigitsForType(String encodingType) {
|
||||||
|
int digitType = toInt(encodingType);
|
||||||
|
if (digitType != 0) {
|
||||||
|
tokenType = TokenType.Default;
|
||||||
|
return digitType;
|
||||||
|
}
|
||||||
|
switch (encodingType) {
|
||||||
|
case "S":
|
||||||
|
case "steam":
|
||||||
|
tokenType = TokenType.Steam;
|
||||||
|
return 5;
|
||||||
|
default:
|
||||||
|
tokenType = TokenType.Default;
|
||||||
|
return DEFAULT_DIGITS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,23 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
* it under the terms of the GNU General Public License as published by
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* License, or (at your option) any later version.
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
*
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
* KeePass DX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* General Public License for more details.
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License along with KeePass DX. If not,
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.view;
|
package com.kunzisoft.keepass.view;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
@@ -31,16 +30,14 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R;
|
import com.kunzisoft.keepass.R;
|
||||||
import com.kunzisoft.keepass.database.security.ProtectedString;
|
import com.kunzisoft.keepass.database.security.ProtectedString;
|
||||||
|
import com.kunzisoft.keepass.totp.*;
|
||||||
import com.kunzisoft.keepass.utils.Util;
|
import com.kunzisoft.keepass.utils.Util;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
public class EntryContentsView extends LinearLayout {
|
public class EntryContentsView extends LinearLayout {
|
||||||
|
|
||||||
private boolean fontInVisibility;
|
private boolean fontInVisibility;
|
||||||
private int colorAccent;
|
private int colorAccent;
|
||||||
|
|
||||||
@@ -52,6 +49,11 @@ public class EntryContentsView extends LinearLayout {
|
|||||||
private TextView passwordView;
|
private TextView passwordView;
|
||||||
private ImageView passwordActionView;
|
private ImageView passwordActionView;
|
||||||
|
|
||||||
|
private View totpContainerView;
|
||||||
|
private TextView totpView;
|
||||||
|
private ImageView totpActionView;
|
||||||
|
private String totpCurrentToken;
|
||||||
|
|
||||||
private View urlContainerView;
|
private View urlContainerView;
|
||||||
private TextView urlView;
|
private TextView urlView;
|
||||||
|
|
||||||
@@ -68,30 +70,31 @@ public class EntryContentsView extends LinearLayout {
|
|||||||
private TextView lastAccessDateView;
|
private TextView lastAccessDateView;
|
||||||
private TextView expiresDateView;
|
private TextView expiresDateView;
|
||||||
|
|
||||||
public EntryContentsView(Context context) {
|
public EntryContentsView(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public EntryContentsView(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
|
|
||||||
fontInVisibility = false;
|
public EntryContentsView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
|
||||||
|
fontInVisibility = false;
|
||||||
|
|
||||||
dateFormat = android.text.format.DateFormat.getDateFormat(context);
|
dateFormat = android.text.format.DateFormat.getDateFormat(context);
|
||||||
timeFormat = android.text.format.DateFormat.getTimeFormat(context);
|
timeFormat = android.text.format.DateFormat.getTimeFormat(context);
|
||||||
|
|
||||||
inflate(context);
|
inflate(context);
|
||||||
|
|
||||||
int[] attrColorAccent = {R.attr.colorAccentCompat};
|
int[] attrColorAccent = {R.attr.colorAccentCompat};
|
||||||
TypedArray taColorAccent = context.getTheme().obtainStyledAttributes(attrColorAccent);
|
TypedArray taColorAccent = context.getTheme().obtainStyledAttributes(attrColorAccent);
|
||||||
this.colorAccent = taColorAccent.getColor(0, Color.BLACK);
|
this.colorAccent = taColorAccent.getColor(0, Color.BLACK);
|
||||||
taColorAccent.recycle();
|
taColorAccent.recycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void inflate(Context context) {
|
private void inflate(Context context) {
|
||||||
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
LayoutInflater inflater =
|
||||||
assert inflater != null;
|
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||||
inflater.inflate(R.layout.entry_view_contents, this);
|
assert inflater != null;
|
||||||
|
inflater.inflate(R.layout.entry_view_contents, this);
|
||||||
|
|
||||||
userNameContainerView = findViewById(R.id.entry_user_name_container);
|
userNameContainerView = findViewById(R.id.entry_user_name_container);
|
||||||
userNameView = findViewById(R.id.entry_user_name);
|
userNameView = findViewById(R.id.entry_user_name);
|
||||||
@@ -104,6 +107,10 @@ public class EntryContentsView extends LinearLayout {
|
|||||||
urlContainerView = findViewById(R.id.entry_url_container);
|
urlContainerView = findViewById(R.id.entry_url_container);
|
||||||
urlView = findViewById(R.id.entry_url);
|
urlView = findViewById(R.id.entry_url);
|
||||||
|
|
||||||
|
totpContainerView = findViewById(R.id.entry_totp_container);
|
||||||
|
totpView = findViewById(R.id.entry_totp);
|
||||||
|
totpActionView = findViewById(R.id.entry_totp_action_image);
|
||||||
|
|
||||||
commentContainerView = findViewById(R.id.entry_comment_container);
|
commentContainerView = findViewById(R.id.entry_comment_container);
|
||||||
commentView = findViewById(R.id.entry_comment);
|
commentView = findViewById(R.id.entry_comment);
|
||||||
|
|
||||||
@@ -113,13 +120,13 @@ public class EntryContentsView extends LinearLayout {
|
|||||||
modificationDateView = findViewById(R.id.entry_modified);
|
modificationDateView = findViewById(R.id.entry_modified);
|
||||||
lastAccessDateView = findViewById(R.id.entry_accessed);
|
lastAccessDateView = findViewById(R.id.entry_accessed);
|
||||||
expiresDateView = findViewById(R.id.entry_expires);
|
expiresDateView = findViewById(R.id.entry_expires);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void applyFontVisibilityToFields(boolean fontInVisibility) {
|
public void applyFontVisibilityToFields(boolean fontInVisibility) {
|
||||||
this.fontInVisibility = fontInVisibility;
|
this.fontInVisibility = fontInVisibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void assignUserName(String userName) {
|
public void assignUserName(String userName) {
|
||||||
if (userName != null && !userName.isEmpty()) {
|
if (userName != null && !userName.isEmpty()) {
|
||||||
userNameContainerView.setVisibility(VISIBLE);
|
userNameContainerView.setVisibility(VISIBLE);
|
||||||
userNameView.setText(userName);
|
userNameView.setText(userName);
|
||||||
@@ -145,7 +152,8 @@ public class EntryContentsView extends LinearLayout {
|
|||||||
if (fontInVisibility)
|
if (fontInVisibility)
|
||||||
Util.applyFontVisibilityTo(getContext(), passwordView);
|
Util.applyFontVisibilityTo(getContext(), passwordView);
|
||||||
if (!allowCopyPassword) {
|
if (!allowCopyPassword) {
|
||||||
passwordActionView.setColorFilter(ContextCompat.getColor(getContext(), R.color.grey_dark));
|
passwordActionView
|
||||||
|
.setColorFilter(ContextCompat.getColor(getContext(), R.color.grey_dark));
|
||||||
} else {
|
} else {
|
||||||
passwordActionView.setColorFilter(colorAccent);
|
passwordActionView.setColorFilter(colorAccent);
|
||||||
}
|
}
|
||||||
@@ -155,13 +163,13 @@ public class EntryContentsView extends LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void assignPasswordCopyListener(OnClickListener onClickListener) {
|
public void assignPasswordCopyListener(OnClickListener onClickListener) {
|
||||||
if (onClickListener == null)
|
if (onClickListener == null)
|
||||||
setClickable(false);
|
setClickable(false);
|
||||||
passwordActionView.setOnClickListener(onClickListener);
|
passwordActionView.setOnClickListener(onClickListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPasswordPresent() {
|
public boolean isPasswordPresent() {
|
||||||
return passwordContainerView.getVisibility() == VISIBLE;
|
return passwordContainerView.getVisibility() == VISIBLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean atLeastOneFieldProtectedPresent() {
|
public boolean atLeastOneFieldProtectedPresent() {
|
||||||
@@ -174,7 +182,7 @@ public class EntryContentsView extends LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setHiddenPasswordStyle(boolean hiddenStyle) {
|
public void setHiddenPasswordStyle(boolean hiddenStyle) {
|
||||||
if ( !hiddenStyle ) {
|
if (!hiddenStyle) {
|
||||||
passwordView.setTransformationMethod(null);
|
passwordView.setTransformationMethod(null);
|
||||||
} else {
|
} else {
|
||||||
passwordView.setTransformationMethod(PasswordTransformationMethod.getInstance());
|
passwordView.setTransformationMethod(PasswordTransformationMethod.getInstance());
|
||||||
@@ -196,6 +204,39 @@ public class EntryContentsView extends LinearLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void assignTotp(TotpSettings settings, OnClickListener onClickListener) {
|
||||||
|
if (settings.isConfigured()) {
|
||||||
|
totpContainerView.setVisibility(VISIBLE);
|
||||||
|
|
||||||
|
String totp = settings.getToken();
|
||||||
|
if (totp.isEmpty()) {
|
||||||
|
totpView.setText(getContext().getString(R.string.error_invalid_TOTP));
|
||||||
|
totpActionView
|
||||||
|
.setColorFilter(ContextCompat.getColor(getContext(), R.color.grey_dark));
|
||||||
|
assignTotpCopyListener(null);
|
||||||
|
} else {
|
||||||
|
assignTotpCopyListener(onClickListener);
|
||||||
|
totpCurrentToken = settings.getToken();
|
||||||
|
final Handler totpHandler = new Handler();
|
||||||
|
totpHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (settings.shouldRefreshToken()) {
|
||||||
|
totpCurrentToken = settings.getToken();
|
||||||
|
}
|
||||||
|
totpView.setText(getContext().getString(R.string.entry_totp_format,
|
||||||
|
totpCurrentToken, settings.getSecondsRemaining()));
|
||||||
|
totpHandler.postDelayed(this, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assignTotpCopyListener(OnClickListener onClickListener) {
|
||||||
|
totpActionView.setOnClickListener(onClickListener);
|
||||||
|
}
|
||||||
|
|
||||||
public void assignComment(String comment) {
|
public void assignComment(String comment) {
|
||||||
if (comment != null && !comment.isEmpty()) {
|
if (comment != null && !comment.isEmpty()) {
|
||||||
commentContainerView.setVisibility(VISIBLE);
|
commentContainerView.setVisibility(VISIBLE);
|
||||||
@@ -207,12 +248,15 @@ public class EntryContentsView extends LinearLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addExtraField(String title, ProtectedString value, boolean showAction, OnClickListener onActionClickListener) {
|
public void addExtraField(String title, ProtectedString value, boolean showAction,
|
||||||
|
OnClickListener onActionClickListener) {
|
||||||
EntryCustomField entryCustomField;
|
EntryCustomField entryCustomField;
|
||||||
if (value.isProtected())
|
if (value.isProtected())
|
||||||
entryCustomField = new EntryCustomFieldProtected(getContext(), null, title, value, showAction, onActionClickListener);
|
entryCustomField = new EntryCustomFieldProtected(getContext(), null, title, value,
|
||||||
else
|
showAction, onActionClickListener);
|
||||||
entryCustomField = new EntryCustomField(getContext(), null, title, value, showAction, onActionClickListener);
|
else
|
||||||
|
entryCustomField = new EntryCustomField(getContext(), null, title, value, showAction,
|
||||||
|
onActionClickListener);
|
||||||
entryCustomField.applyFontVisibility(fontInVisibility);
|
entryCustomField.applyFontVisibility(fontInVisibility);
|
||||||
extrasView.addView(entryCustomField);
|
extrasView.addView(entryCustomField);
|
||||||
}
|
}
|
||||||
@@ -245,9 +289,9 @@ public class EntryContentsView extends LinearLayout {
|
|||||||
expiresDateView.setText(constString);
|
expiresDateView.setText(constString);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected LayoutParams generateDefaultLayoutParams() {
|
protected LayoutParams generateDefaultLayoutParams() {
|
||||||
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,38 @@
|
|||||||
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
|
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- TOTP -->
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/entry_totp_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone">
|
||||||
|
<android.support.v7.widget.AppCompatTextView
|
||||||
|
android:id="@+id/entry_totp_label"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/entry_totp"
|
||||||
|
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
|
||||||
|
<android.support.v7.widget.AppCompatTextView
|
||||||
|
android:id="@+id/entry_totp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/entry_totp_label"
|
||||||
|
android:layout_toLeftOf="@+id/entry_totp_action_image"
|
||||||
|
android:layout_toStartOf="@+id/entry_totp_action_image"
|
||||||
|
android:textIsSelectable="true"
|
||||||
|
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
|
||||||
|
<android.support.v7.widget.AppCompatImageView
|
||||||
|
android:id="@+id/entry_totp_action_image"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_below="@+id/entry_totp_label"
|
||||||
|
android:src="@drawable/ic_content_copy_white_24dp"
|
||||||
|
android:tint="?attr/colorAccent" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
<!-- Comment -->
|
<!-- Comment -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/entry_comment_container"
|
android:id="@+id/entry_comment_container"
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
<string name="entry_password">Password</string>
|
<string name="entry_password">Password</string>
|
||||||
<string name="entry_save">Save</string>
|
<string name="entry_save">Save</string>
|
||||||
<string name="entry_title">Title</string>
|
<string name="entry_title">Title</string>
|
||||||
|
<string name="entry_totp">TOTP</string>
|
||||||
|
<string name="entry_totp_format">%1$ (%2$)</string>
|
||||||
<string name="entry_url">URL</string>
|
<string name="entry_url">URL</string>
|
||||||
<string name="entry_user_name">Username</string>
|
<string name="entry_user_name">Username</string>
|
||||||
<string name="error_arc4">The ARCFOUR stream cipher is not supported.</string>
|
<string name="error_arc4">The ARCFOUR stream cipher is not supported.</string>
|
||||||
@@ -75,6 +77,7 @@
|
|||||||
<string name="error_file_not_create">Could not create file:</string>
|
<string name="error_file_not_create">Could not create file:</string>
|
||||||
<string name="error_invalid_db">Invalid database or unrecognized master key.</string>
|
<string name="error_invalid_db">Invalid database or unrecognized master key.</string>
|
||||||
<string name="error_invalid_path">Invalid path.</string>
|
<string name="error_invalid_path">Invalid path.</string>
|
||||||
|
<string name="error_invalid_TOTP">Invalid TOTP secret.</string>
|
||||||
<string name="error_no_name">A name is required.</string>
|
<string name="error_no_name">A name is required.</string>
|
||||||
<string name="error_nokeyfile">A keyfile is required.</string>
|
<string name="error_nokeyfile">A keyfile is required.</string>
|
||||||
<string name="error_out_of_memory">The phone ran out of memory while parsing your database. It may be too large for your device.</string>
|
<string name="error_out_of_memory">The phone ran out of memory while parsing your database. It may be too large for your device.</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user