Add Read support for TOTP Tokens

This commit is contained in:
somkun
2018-09-16 22:11:23 -07:00
parent ff6b0eee6c
commit 9558fcaf21
9 changed files with 680 additions and 263 deletions

7
.gitignore vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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();
} }
} }

View 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;
}
}

View 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;
}
}
}

View File

@@ -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);
} }
} }

View File

@@ -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"

View File

@@ -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>