From 9558fcaf2176ff3e835c186c653d1442a237ae6f Mon Sep 17 00:00:00 2001 From: somkun Date: Sun, 16 Sep 2018 22:11:23 -0700 Subject: [PATCH] Add Read support for TOTP Tokens --- .gitignore | 7 + CONTRIBUTORS | 1 + app/build.gradle | 2 + .../keepass/activities/EntryActivity.java | 432 +++++++++--------- .../kunzisoft/keepass/totp/TotpGenerator.java | 111 +++++ .../kunzisoft/keepass/totp/TotpSettings.java | 221 +++++++++ .../keepass/view/EntryContentsView.java | 134 ++++-- .../main/res/layout/entry_view_contents.xml | 32 ++ app/src/main/res/values/strings.xml | 3 + 9 files changed, 680 insertions(+), 263 deletions(-) create mode 100644 app/src/main/java/com/kunzisoft/keepass/totp/TotpGenerator.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/totp/TotpSettings.java diff --git a/.gitignore b/.gitignore index be230663e..b6de83e58 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,13 @@ proguard/ # Android Studio captures folder captures/ +# Eclipse/VS Code +.project +.settings/* +*/.project +*/.classpath +*/.settings/* + # Intellij *.iml .idea/workspace.xml diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 52ccfe1a4..56ffe9af2 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -15,6 +15,7 @@ vhschlenker bumper314 - Samsung multiwindow support Hans Cappelle - fingerprint sensor integration Jeremy Jamet - Keepass DX Material Design - Patches +somkun - TOTP support Translations: Diego Pierotto - Italian diff --git a/app/build.gradle b/app/build.gradle index f73ac9445..c8d24f86f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -103,6 +103,8 @@ dependencies { annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion" // Apache Commons Collections implementation 'commons-collections:commons-collections:3.2.1' + // Apache Commons Codec + implementation 'commons-codec:commons-codec:1.11' // Base64 implementation 'biz.source_code:base64coder:2010-12-19' implementation 'com.google.code.gson:gson:2.8.4' diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java index 779cd1205..8a0b3edda 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java @@ -1,21 +1,19 @@ /* * * 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 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. + * 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 . + * You should have received a copy of the GNU General Public License along with KeePass DX. If not, + * see . * */ package com.kunzisoft.keepass.activities; @@ -37,7 +35,6 @@ import android.view.MenuItem; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; - import com.getkeepsafe.taptargetview.TapTarget; import com.getkeepsafe.taptargetview.TapTargetView; 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.SettingsAutofillActivity; import com.kunzisoft.keepass.timeout.ClipboardHelper; +import com.kunzisoft.keepass.totp.*; import com.kunzisoft.keepass.utils.EmptyUtils; import com.kunzisoft.keepass.utils.MenuUtil; import com.kunzisoft.keepass.utils.Types; import com.kunzisoft.keepass.utils.Util; import com.kunzisoft.keepass.view.EntryContentsView; - import java.util.ArrayList; import java.util.Date; import java.util.UUID; - import static com.kunzisoft.keepass.settings.PreferencesUtil.isClipboardNotificationsEnable; import static com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields; public class EntryActivity extends LockingHideActivity { 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 EntryContentsView entryContentsView; + private EntryContentsView entryContentsView; private Toolbar toolbar; - - protected PwEntry mEntry; - private boolean mShowPassword; - private ClipboardHelper clipboardHelper; - private boolean firstLaunchOfActivity; + protected PwEntry mEntry; + 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) { if (LockingActivity.checkTimeIsAllowedOrFinish(act)) { @@ -94,60 +91,62 @@ public class EntryActivity extends LockingHideActivity { } } - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); setContentView(R.layout.entry_view); toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); assert getSupportActionBar() != null; - getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); + getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); - Database db = App.getDB(); - // Likely the app has been killed exit the activity - if ( ! db.getLoaded() ) { - finish(); - return; - } - readOnly = db.isReadOnly() || readOnly; + Database db = App.getDB(); + // Likely the app has been killed exit the activity + if (!db.getLoaded()) { + finish(); + return; + } + readOnly = db.isReadOnly() || readOnly; mShowPassword = !PreferencesUtil.isPasswordMask(this); - // Get Entry from UUID - Intent i = getIntent(); - UUID uuid = Types.bytestoUUID(i.getByteArrayExtra(KEY_ENTRY)); - mEntry = db.getPwDatabase().getEntryByUUIDId(uuid); - if (mEntry == null) { - Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show(); - finish(); - return; - } + // Get Entry from UUID + Intent i = getIntent(); + UUID uuid = Types.bytestoUUID(i.getByteArrayExtra(KEY_ENTRY)); + mEntry = db.getPwDatabase().getEntryByUUIDId(uuid); + if (mEntry == null) { + Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show(); + finish(); + return; + } + mTotpSettings = new TotpSettings(mEntry); // Retrieve the textColor to tint the icon int[] attrs = {R.attr.textColorInverse}; TypedArray ta = getTheme().obtainStyledAttributes(attrs); iconColor = ta.getColor(0, Color.WHITE); - - // Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set - invalidateOptionsMenu(); - - // Update last access time. - mEntry.touch(false, false); + + // Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set + invalidateOptionsMenu(); + + // Update last access time. + mEntry.touch(false, false); // Get views titleIconView = findViewById(R.id.entry_icon); titleView = findViewById(R.id.entry_title); entryContentsView = findViewById(R.id.entry_contents); - entryContentsView.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)); + entryContentsView + .applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)); // Init the clipboard helper clipboardHelper = new ClipboardHelper(this); firstLaunchOfActivity = true; - } + } @Override protected void onResume() { @@ -160,76 +159,68 @@ public class EntryActivity extends LockingHideActivity { // Start to manage field reference to copy a value from ref mEntry.startToManageFieldReferences(App.getDB().getPwDatabase()); - boolean containsUsernameToCopy = - mEntry.getUsername().length() > 0; - boolean containsPasswordToCopy = - (mEntry.getPassword().length() > 0 - && PreferencesUtil.allowCopyPasswordAndProtectedFields(this)); - boolean containsExtraFieldToCopy = - (mEntry.allowExtraFields() - && ((mEntry.containsCustomFields() - && mEntry.containsCustomFieldsNotProtected()) - || (mEntry.containsCustomFields() - && mEntry.containsCustomFieldsProtected() - && PreferencesUtil.allowCopyPasswordAndProtectedFields(this)) - ) - ); + boolean containsUsernameToCopy = mEntry.getUsername().length() > 0; + boolean containsPasswordToCopy = (mEntry.getPassword().length() > 0 + && PreferencesUtil.allowCopyPasswordAndProtectedFields(this)); + boolean containsExtraFieldToCopy = (mEntry.allowExtraFields() + && ((mEntry.containsCustomFields() && mEntry.containsCustomFieldsNotProtected()) + || (mEntry.containsCustomFields() && mEntry.containsCustomFieldsProtected() + && PreferencesUtil.allowCopyPasswordAndProtectedFields(this)))); // If notifications enabled in settings // Don't if application timeout - if (firstLaunchOfActivity && !App.isShutdown() && isClipboardNotificationsEnable(getApplicationContext())) { - if (containsUsernameToCopy - || containsPasswordToCopy - || containsExtraFieldToCopy - ) { + if (firstLaunchOfActivity && !App.isShutdown() + && isClipboardNotificationsEnable(getApplicationContext())) { + if (containsUsernameToCopy || containsPasswordToCopy || containsExtraFieldToCopy) { // username already copied, waiting for user's action before copy password. Intent intent = new Intent(this, NotificationCopyingService.class); intent.setAction(NotificationCopyingService.ACTION_NEW_NOTIFICATION); if (mEntry.getTitle() != null) - intent.putExtra(NotificationCopyingService.EXTRA_ENTRY_TITLE, mEntry.getTitle()); + intent.putExtra(NotificationCopyingService.EXTRA_ENTRY_TITLE, + mEntry.getTitle()); // Construct notification fields ArrayList notificationFields = new ArrayList<>(); // Add username if exists to notifications if (containsUsernameToCopy) notificationFields.add( - new NotificationField( - NotificationField.NotificationFieldId.USERNAME, - mEntry.getUsername(), - getResources())); + new NotificationField(NotificationField.NotificationFieldId.USERNAME, + mEntry.getUsername(), getResources())); // Add password to notifications if (containsPasswordToCopy) { notificationFields.add( - new NotificationField( - NotificationField.NotificationFieldId.PASSWORD, - mEntry.getPassword(), - getResources())); + new NotificationField(NotificationField.NotificationFieldId.PASSWORD, + mEntry.getPassword(), getResources())); } // Add extra fields if (containsExtraFieldToCopy) { try { - mEntry.getFields().doActionToAllCustomProtectedField(new ExtraFields.ActionProtected() { - private int anonymousFieldNumber = 0; - @Override - public void doAction(String key, ProtectedString value) { - //If value is not protected or allowed - if (!value.isProtected() || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this)) { - notificationFields.add( - new NotificationField( - NotificationField.NotificationFieldId.getAnonymousFieldId()[anonymousFieldNumber], - value.toString(), - key, - getResources())); - anonymousFieldNumber++; - } - } - }); + mEntry.getFields().doActionToAllCustomProtectedField( + new ExtraFields.ActionProtected() { + private int anonymousFieldNumber = 0; + + @Override + public void doAction(String key, ProtectedString value) { + // If value is not protected or allowed + if (!value.isProtected() || PreferencesUtil + .allowCopyPasswordAndProtectedFields( + EntryActivity.this)) { + notificationFields.add(new NotificationField( + NotificationField.NotificationFieldId + .getAnonymousFieldId()[anonymousFieldNumber], + value.toString(), key, getResources())); + anonymousFieldNumber++; + } + } + }); } catch (ArrayIndexOutOfBoundsException e) { - Log.w(TAG, "Only " + NotificationField.NotificationFieldId.getAnonymousFieldId().length + - " anonymous notifications are available"); + Log.w(TAG, "Only " + + NotificationField.NotificationFieldId.getAnonymousFieldId().length + + " anonymous notifications are available"); } } // Add notifications - intent.putParcelableArrayListExtra(NotificationCopyingService.EXTRA_FIELDS, notificationFields); + intent.putParcelableArrayListExtra(NotificationCopyingService.EXTRA_FIELDS, + notificationFields); startService(intent); } @@ -239,27 +230,28 @@ public class EntryActivity extends LockingHideActivity { } /** - * Check and display learning views - * Displays the explanation for copying a field and editing an entry + * Check and display learning views Displays the explanation for copying a field and editing an + * entry */ private void checkAndPerformedEducation(Menu menu) { if (PreferencesUtil.isEducationScreensEnabled(this)) { if (entryContentsView != null && entryContentsView.isUserNamePresent() && !PreferencesUtil.isEducationCopyUsernamePerformed(this)) { - TapTargetView.showFor(this, - TapTarget.forView(findViewById(R.id.entry_user_name_action_image), - getString(R.string.education_field_copy_title), - getString(R.string.education_field_copy_summary)) - .textColorInt(Color.WHITE) - .tintTarget(false) - .cancelable(true), + TapTargetView.showFor( + this, + TapTarget + .forView(findViewById(R.id.entry_user_name_action_image), + getString(R.string.education_field_copy_title), + getString(R.string.education_field_copy_summary)) + .textColorInt(Color.WHITE).tintTarget(false).cancelable(true), new TapTargetView.Listener() { @Override public void onTargetClick(TapTargetView view) { super.onTargetClick(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))); } @Override @@ -267,40 +259,43 @@ public class EntryActivity extends LockingHideActivity { super.onOuterCircleClick(view); view.dismiss(false); // Launch autofill settings - startActivity(new Intent(EntryActivity.this, SettingsAutofillActivity.class)); + startActivity(new Intent(EntryActivity.this, + SettingsAutofillActivity.class)); } }); - PreferencesUtil.saveEducationPreference(this, - R.string.education_copy_username_key); + PreferencesUtil.saveEducationPreference(this, R.string.education_copy_username_key); } else if (!PreferencesUtil.isEducationEntryEditPerformed(this)) { try { - TapTargetView.showFor(this, - TapTarget.forToolbarMenuItem(toolbar, R.id.menu_edit, - getString(R.string.education_entry_edit_title), - getString(R.string.education_entry_edit_summary)) - .textColorInt(Color.WHITE) - .tintTarget(true) - .cancelable(true), - new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - MenuItem editItem = menu.findItem(R.id.menu_edit); - onOptionsItemSelected(editItem); - } + TapTargetView + .showFor(this, + TapTarget + .forToolbarMenuItem(toolbar, R.id.menu_edit, + getString(R.string.education_entry_edit_title), + getString( + R.string.education_entry_edit_summary)) + .textColorInt(Color.WHITE).tintTarget(true) + .cancelable(true), + new TapTargetView.Listener() { + @Override + public void onTargetClick(TapTargetView view) { + super.onTargetClick(view); + MenuItem editItem = menu.findItem(R.id.menu_edit); + onOptionsItemSelected(editItem); + } - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - view.dismiss(false); - // Open Keepass doc to create field references - Intent browserIntent = new Intent(Intent.ACTION_VIEW, - Uri.parse(getString(R.string.field_references_url))); - startActivity(browserIntent); - } - }); + @Override + public void onOuterCircleClick(TapTargetView view) { + super.onOuterCircleClick(view); + view.dismiss(false); + // Open Keepass doc to create field references + Intent browserIntent = new Intent(Intent.ACTION_VIEW, + Uri.parse(getString( + R.string.field_references_url))); + startActivity(browserIntent); + } + }); PreferencesUtil.saveEducationPreference(this, R.string.education_entry_edit_key); } catch (Exception e) { @@ -311,50 +306,49 @@ public class EntryActivity extends LockingHideActivity { } } - protected void fillData() { - Database db = App.getDB(); - PwDatabase pm = db.getPwDatabase(); + protected void fillData() { + Database db = App.getDB(); + PwDatabase pm = db.getPwDatabase(); - mEntry.startToManageFieldReferences(pm); + mEntry.startToManageFieldReferences(pm); // Assign title icon db.getDrawFactory().assignDatabaseIconTo(this, titleIconView, mEntry.getIcon(), iconColor); - // Assign title text + // Assign title text titleView.setText(mEntry.getVisualTitle()); // Assign basic fields entryContentsView.assignUserName(mEntry.getUsername()); - entryContentsView.assignUserNameCopyListener(view -> - clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(), - getString(R.string.copy_field, getString(R.string.entry_user_name))) - ); + entryContentsView.assignUserNameCopyListener( + view -> clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(), + getString(R.string.copy_field, getString(R.string.entry_user_name)))); boolean allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this); - entryContentsView.assignPassword(mEntry.getPassword(), allowCopyPassword); - if (allowCopyPassword) { - entryContentsView.assignPasswordCopyListener(view -> - clipboardHelper.timeoutCopyToClipboard(mEntry.getPassword(), - getString(R.string.copy_field, getString(R.string.entry_password))) - ); + entryContentsView.assignPassword(mEntry.getPassword(), allowCopyPassword); + if (allowCopyPassword) { + entryContentsView.assignPasswordCopyListener( + view -> clipboardHelper.timeoutCopyToClipboard(mEntry.getPassword(), + getString(R.string.copy_field, getString(R.string.entry_password)))); } else { - // If dialog not already shown + // If dialog not already shown if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) { entryContentsView.assignPasswordCopyListener(v -> { - String message = getString(R.string.allow_copy_password_warning) + - "\n\n" + - getString(R.string.clipboard_warning); + String message = getString(R.string.allow_copy_password_warning) + "\n\n" + + getString(R.string.clipboard_warning); AlertDialog warningDialog = new AlertDialog.Builder(EntryActivity.this) .setMessage(message).create(); warningDialog.setButton(AlertDialog.BUTTON1, getText(android.R.string.ok), (dialog, which) -> { - PreferencesUtil.setAllowCopyPasswordAndProtectedFields(EntryActivity.this, true); + PreferencesUtil.setAllowCopyPasswordAndProtectedFields( + EntryActivity.this, true); dialog.dismiss(); fillData(); }); warningDialog.setButton(AlertDialog.BUTTON2, getText(android.R.string.cancel), (dialog, which) -> { - PreferencesUtil.setAllowCopyPasswordAndProtectedFields(EntryActivity.this, false); + PreferencesUtil.setAllowCopyPasswordAndProtectedFields( + EntryActivity.this, false); dialog.dismiss(); fillData(); }); @@ -367,84 +361,87 @@ public class EntryActivity extends LockingHideActivity { 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.assignComment(mEntry.getNotes()); // Assign custom fields - if (mEntry.allowExtraFields()) { - entryContentsView.clearExtraFields(); + if (mEntry.allowExtraFields()) { + entryContentsView.clearExtraFields(); - mEntry.getFields().doActionToAllCustomProtectedField((label, value) -> { - boolean showAction = (!value.isProtected() || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this)); - entryContentsView.addExtraField(label, value, showAction, view -> - clipboardHelper.timeoutCopyToClipboard( - value.toString(), - getString(R.string.copy_field, label) - ) - ); + mEntry.getFields().doActionToAllCustomProtectedField((label, value) -> { + boolean showAction = (!value.isProtected() + || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this)); + entryContentsView.addExtraField(label, value, showAction, + view -> clipboardHelper.timeoutCopyToClipboard(value.toString(), + getString(R.string.copy_field, label))); }); - } + } // Assign dates entryContentsView.assignCreationDate(mEntry.getCreationTime().getDate()); entryContentsView.assignModificationDate(mEntry.getLastModificationTime().getDate()); entryContentsView.assignLastAccessDate(mEntry.getLastAccessTime().getDate()); - Date expires = mEntry.getExpiryTime().getDate(); - if ( mEntry.isExpires() ) { - entryContentsView.assignExpiresDate(expires); - } else { + Date expires = mEntry.getExpiryTime().getDate(); + if (mEntry.isExpires()) { + entryContentsView.assignExpiresDate(expires); + } else { entryContentsView.assignExpiresDate(getString(R.string.never)); - } + } mEntry.stopToManageFieldReferences(); - } + } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE: fillData(); break; } - } + } - private void changeShowPasswordIcon(MenuItem togglePassword) { - if ( mShowPassword ) { - togglePassword.setTitle(R.string.menu_hide_password); - togglePassword.setIcon(R.drawable.ic_visibility_off_white_24dp); - } else { - togglePassword.setTitle(R.string.menu_showpass); - togglePassword.setIcon(R.drawable.ic_visibility_white_24dp); - } - } + private void changeShowPasswordIcon(MenuItem togglePassword) { + if (mShowPassword) { + togglePassword.setTitle(R.string.menu_hide_password); + togglePassword.setIcon(R.drawable.ic_visibility_off_white_24dp); + } else { + togglePassword.setTitle(R.string.menu_showpass); + togglePassword.setIcon(R.drawable.ic_visibility_white_24dp); + } + } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - - MenuInflater inflater = getMenuInflater(); + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + + MenuInflater inflater = getMenuInflater(); MenuUtil.contributionMenuInflater(inflater, menu); - inflater.inflate(R.menu.entry, menu); - inflater.inflate(R.menu.database_lock, menu); + inflater.inflate(R.menu.entry, menu); + inflater.inflate(R.menu.database_lock, menu); if (readOnly) { - MenuItem edit = menu.findItem(R.id.menu_edit); + MenuItem edit = menu.findItem(R.id.menu_edit); if (edit != null) edit.setVisible(false); } - MenuItem togglePassword = menu.findItem(R.id.menu_toggle_pass); - if (entryContentsView != null && togglePassword != null) { - if (entryContentsView.isPasswordPresent() || entryContentsView.atLeastOneFieldProtectedPresent()) { + MenuItem togglePassword = menu.findItem(R.id.menu_toggle_pass); + if (entryContentsView != null && togglePassword != null) { + if (entryContentsView.isPasswordPresent() + || entryContentsView.atLeastOneFieldProtectedPresent()) { changeShowPasswordIcon(togglePassword); } else { togglePassword.setVisible(false); } } - - MenuItem gotoUrl = menu.findItem(R.id.menu_goto_url); - if (gotoUrl != null) { + + MenuItem gotoUrl = menu.findItem(R.id.menu_goto_url); + if (gotoUrl != null) { // In API >= 11 onCreateOptionsMenu may be called before onCreate completes // so mEntry may not be set if (mEntry == null) { @@ -460,13 +457,13 @@ public class EntryActivity extends LockingHideActivity { // Show education views new Handler().post(() -> checkAndPerformedEducation(menu)); - - return true; - } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch ( item.getItemId() ) { + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { case R.id.menu_contribute: return MenuUtil.onContributionItemSelected(this); @@ -479,13 +476,13 @@ public class EntryActivity extends LockingHideActivity { case R.id.menu_edit: EntryEditActivity.launch(EntryActivity.this, mEntry); return true; - + case R.id.menu_goto_url: String url; url = mEntry.getUrl(); // Default http:// if no protocol specified - if ( ! url.contains("://") ) { + if (!url.contains("://")) { url = "http://" + url; } @@ -495,28 +492,27 @@ public class EntryActivity extends LockingHideActivity { Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show(); } return true; - + case R.id.menu_lock: lockAndExit(); 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) } - return super.onOptionsItemSelected(item); - } + return super.onOptionsItemSelected(item); + } @Override public void finish() { // Transit data in previous Activity after an update - /* - TODO Slowdown when add entry as result - Intent intent = new Intent(); - intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry); - setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent); - */ + /* + * TODO Slowdown when add entry as result Intent intent = new Intent(); + * intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry); + * setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent); + */ super.finish(); } } diff --git a/app/src/main/java/com/kunzisoft/keepass/totp/TotpGenerator.java b/app/src/main/java/com/kunzisoft/keepass/totp/TotpGenerator.java new file mode 100644 index 000000000..aa45c2de0 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/totp/TotpGenerator.java @@ -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 . + * + * 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; + } + +} diff --git a/app/src/main/java/com/kunzisoft/keepass/totp/TotpSettings.java b/app/src/main/java/com/kunzisoft/keepass/totp/TotpSettings.java new file mode 100644 index 000000000..35a7d0f40 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/totp/TotpSettings.java @@ -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 . + * + * 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 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 breakDownKeyValuePairs(String pairs) { + String[] elements = pairs.split("&"); + HashMap output = new HashMap(); + 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; + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.java b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.java index e6a299e4d..7780459a4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.java +++ b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.java @@ -1,24 +1,23 @@ /* * 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 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. + * 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 . + * You should have received a copy of the GNU General Public License along with KeePass DX. If not, + * see . * */ package com.kunzisoft.keepass.view; +import android.os.Handler; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; @@ -31,16 +30,14 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; - import com.kunzisoft.keepass.R; import com.kunzisoft.keepass.database.security.ProtectedString; +import com.kunzisoft.keepass.totp.*; import com.kunzisoft.keepass.utils.Util; - import java.text.DateFormat; import java.util.Date; public class EntryContentsView extends LinearLayout { - private boolean fontInVisibility; private int colorAccent; @@ -52,6 +49,11 @@ public class EntryContentsView extends LinearLayout { private TextView passwordView; private ImageView passwordActionView; + private View totpContainerView; + private TextView totpView; + private ImageView totpActionView; + private String totpCurrentToken; + private View urlContainerView; private TextView urlView; @@ -68,30 +70,31 @@ public class EntryContentsView extends LinearLayout { private TextView lastAccessDateView; private TextView expiresDateView; - public EntryContentsView(Context context) { - this(context, null); - } - - public EntryContentsView(Context context, AttributeSet attrs) { - super(context, attrs); + public EntryContentsView(Context context) { + this(context, null); + } - fontInVisibility = false; + public EntryContentsView(Context context, AttributeSet attrs) { + super(context, attrs); + + fontInVisibility = false; dateFormat = android.text.format.DateFormat.getDateFormat(context); timeFormat = android.text.format.DateFormat.getTimeFormat(context); - - inflate(context); + + inflate(context); int[] attrColorAccent = {R.attr.colorAccentCompat}; TypedArray taColorAccent = context.getTheme().obtainStyledAttributes(attrColorAccent); this.colorAccent = taColorAccent.getColor(0, Color.BLACK); taColorAccent.recycle(); - } - - private void inflate(Context context) { - LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - assert inflater != null; - inflater.inflate(R.layout.entry_view_contents, this); + } + + private void inflate(Context context) { + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + assert inflater != null; + inflater.inflate(R.layout.entry_view_contents, this); userNameContainerView = findViewById(R.id.entry_user_name_container); userNameView = findViewById(R.id.entry_user_name); @@ -104,6 +107,10 @@ public class EntryContentsView extends LinearLayout { urlContainerView = findViewById(R.id.entry_url_container); 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); commentView = findViewById(R.id.entry_comment); @@ -113,13 +120,13 @@ public class EntryContentsView extends LinearLayout { modificationDateView = findViewById(R.id.entry_modified); lastAccessDateView = findViewById(R.id.entry_accessed); expiresDateView = findViewById(R.id.entry_expires); - } + } public void applyFontVisibilityToFields(boolean fontInVisibility) { this.fontInVisibility = fontInVisibility; } - public void assignUserName(String userName) { + public void assignUserName(String userName) { if (userName != null && !userName.isEmpty()) { userNameContainerView.setVisibility(VISIBLE); userNameView.setText(userName); @@ -145,7 +152,8 @@ public class EntryContentsView extends LinearLayout { if (fontInVisibility) Util.applyFontVisibilityTo(getContext(), passwordView); if (!allowCopyPassword) { - passwordActionView.setColorFilter(ContextCompat.getColor(getContext(), R.color.grey_dark)); + passwordActionView + .setColorFilter(ContextCompat.getColor(getContext(), R.color.grey_dark)); } else { passwordActionView.setColorFilter(colorAccent); } @@ -155,13 +163,13 @@ public class EntryContentsView extends LinearLayout { } public void assignPasswordCopyListener(OnClickListener onClickListener) { - if (onClickListener == null) - setClickable(false); + if (onClickListener == null) + setClickable(false); passwordActionView.setOnClickListener(onClickListener); } public boolean isPasswordPresent() { - return passwordContainerView.getVisibility() == VISIBLE; + return passwordContainerView.getVisibility() == VISIBLE; } public boolean atLeastOneFieldProtectedPresent() { @@ -174,7 +182,7 @@ public class EntryContentsView extends LinearLayout { } public void setHiddenPasswordStyle(boolean hiddenStyle) { - if ( !hiddenStyle ) { + if (!hiddenStyle) { passwordView.setTransformationMethod(null); } else { 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) { if (comment != null && !comment.isEmpty()) { 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; - if (value.isProtected()) - entryCustomField = new EntryCustomFieldProtected(getContext(), null, title, value, showAction, onActionClickListener); - else - entryCustomField = new EntryCustomField(getContext(), null, title, value, showAction, onActionClickListener); + if (value.isProtected()) + entryCustomField = new EntryCustomFieldProtected(getContext(), null, title, value, + showAction, onActionClickListener); + else + entryCustomField = new EntryCustomField(getContext(), null, title, value, showAction, + onActionClickListener); entryCustomField.applyFontVisibility(fontInVisibility); extrasView.addView(entryCustomField); } @@ -245,9 +289,9 @@ public class EntryContentsView extends LinearLayout { expiresDateView.setText(constString); } - @Override - protected LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); - } + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } } diff --git a/app/src/main/res/layout/entry_view_contents.xml b/app/src/main/res/layout/entry_view_contents.xml index d5f80ef6f..674696df4 100644 --- a/app/src/main/res/layout/entry_view_contents.xml +++ b/app/src/main/res/layout/entry_view_contents.xml @@ -115,6 +115,38 @@ style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" /> + + + + + + + Password Save Title + TOTP + %1$ (%2$) URL Username The ARCFOUR stream cipher is not supported. @@ -75,6 +77,7 @@ Could not create file: Invalid database or unrecognized master key. Invalid path. + Invalid TOTP secret. A name is required. A keyfile is required. The phone ran out of memory while parsing your database. It may be too large for your device.