From 646940eb4409e5a04ed628893e515f6534e720d5 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Thu, 7 Sep 2017 16:05:59 +0200 Subject: [PATCH] token-import: add ui for pin change and unlock --- .../ui/base/BaseSecurityTokenActivity.java | 25 ++++-- .../ui/token/ChangePinDialogHelper.java | 76 ++++++++++++++++ .../ui/token/ManageSecurityTokenContract.java | 17 +++- .../ui/token/ManageSecurityTokenFragment.java | 55 ++++++++++-- .../token/ManageSecurityTokenPresenter.java | 87 ++++++++++++++++--- .../src/main/res/layout/admin_pin_dialog.xml | 31 +++++++ .../create_security_token_import_fragment.xml | 79 ++++++++++++++++- .../src/main/res/layout/fake_dialog.xml | 16 ++++ ...ration_activity_toolable_view_animator.xml | 32 ++++++- .../src/main/res/menu/token_debug.xml | 8 ++ .../src/main/res/menu/token_setup.xml | 7 +- OpenKeychain/src/main/res/values/strings.xml | 14 +++ 12 files changed, 413 insertions(+), 34 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ChangePinDialogHelper.java create mode 100644 OpenKeychain/src/main/res/layout/admin_pin_dialog.xml create mode 100644 OpenKeychain/src/main/res/layout/fake_dialog.xml diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java index 8c6f1a989..db7153e3d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java @@ -108,7 +108,7 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity /** * Override to do something when PIN is wrong, e.g., clear passphrases (UI thread) */ - protected void onSecurityTokenPinError(String error) { + protected void onSecurityTokenPinError(String error, SecurityTokenInfo tokeninfo) { onSecurityTokenError(error); } @@ -242,8 +242,16 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity // https://github.com/Yubico/ykneo-openpgp/commit/90c2b91e86fb0e43ee234dd258834e75e3416410 if ((status & (short) 0xFFF0) == 0x63C0) { int tries = status & 0x000F; + + SecurityTokenInfo tokeninfo = null; + try { + tokeninfo = mSecurityTokenHelper.getTokenInfo(); + } catch (IOException e2) { + // don't care + } // hook to do something different when PIN is wrong - onSecurityTokenPinError(getResources().getQuantityString(R.plurals.security_token_error_pin, tries, tries)); + onSecurityTokenPinError( + getResources().getQuantityString(R.plurals.security_token_error_pin, tries, tries), tokeninfo); return; } @@ -256,8 +264,15 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity PW not checked (command not allowed), Secure messaging incorrect (checksum and/or cryptogram) */ // NOTE: Used in ykneo-openpgp >= 1.0.11 for wrong PIN case 0x6982: { + SecurityTokenInfo tokeninfo = null; + try { + tokeninfo = mSecurityTokenHelper.getTokenInfo(); + } catch (IOException e2) { + // don't care + } + // hook to do something different when PIN is wrong - onSecurityTokenPinError(getString(R.string.security_token_error_security_not_satisfied)); + onSecurityTokenPinError(getString(R.string.security_token_error_security_not_satisfied), tokeninfo); break; } /* OpenPGP Card Spec: Selected file in termination state */ @@ -270,14 +285,14 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity // https://github.com/Yubico/ykneo-openpgp/commit/b49ce8241917e7c087a4dab7b2c755420ff4500f case 0x6700: { // hook to do something different when PIN is wrong - onSecurityTokenPinError(getString(R.string.security_token_error_wrong_length)); + onSecurityTokenPinError(getString(R.string.security_token_error_wrong_length), null); break; } /* OpenPGP Card Spec: Incorrect parameters in the data field */ // NOTE: Used in ykneo-openpgp >= 1.0.11 for too short PIN case 0x6A80: { // hook to do something different when PIN is wrong - onSecurityTokenPinError(getString(R.string.security_token_error_bad_data)); + onSecurityTokenPinError(getString(R.string.security_token_error_bad_data), null); break; } /* OpenPGP Card Spec: Authentication method blocked, PW blocked (error counter zero) */ diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ChangePinDialogHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ChangePinDialogHelper.java new file mode 100644 index 000000000..0db53ca2c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ChangePinDialogHelper.java @@ -0,0 +1,76 @@ +package org.sufficientlysecure.keychain.ui.token; + + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnShowListener; +import android.support.annotation.CheckResult; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AlertDialog.Builder; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.EditText; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.token.ManageSecurityTokenContract.ManageSecurityTokenMvpPresenter; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; + + +class ChangePinDialogHelper { + @CheckResult + static AlertDialog createAdminPinDialog(Context context, final ManageSecurityTokenMvpPresenter presenter) { + ContextThemeWrapper themedContext = ThemeChanger.getDialogThemeWrapper(context); + + @SuppressLint("InflateParams") // it's a dialog, no root element + View view = LayoutInflater.from(themedContext).inflate(R.layout.admin_pin_dialog, null, false); + final EditText adminPin = (EditText) view.findViewById(R.id.admin_pin_current); + final EditText newPin = (EditText) view.findViewById(R.id.pin_new); + final EditText newPinRepeat = (EditText) view.findViewById(R.id.pin_new_repeat); + + AlertDialog dialog = new Builder(themedContext) + .setView(view) + .setNegativeButton(R.string.button_cancel, null) + .setPositiveButton(R.string.token_unlock_ok, null).create(); + dialog.setOnShowListener(new OnShowListener() { + @Override + public void onShow(final DialogInterface dialog) { + ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + checkAndHandleInput(adminPin, newPin, newPinRepeat, dialog, presenter); + } + }); + } + }); + + return dialog; + } + + private static void checkAndHandleInput(EditText adminPinView, EditText newPinView, EditText newPinRepeatView, + DialogInterface dialog, ManageSecurityTokenMvpPresenter presenter) { + String adminPin = adminPinView.getText().toString(); + String newPin = newPinView.getText().toString(); + String newPinRepeat = newPinRepeatView.getText().toString(); + + if (adminPin.length() < 8) { + adminPinView.setError(adminPinView.getContext().getString(R.string.token_error_admin_min8)); + return; + } + + if (newPin.length() < 6) { + newPinView.setError(newPinView.getContext().getString(R.string.token_error_pin_min6)); + return; + } + + if (!newPin.equals(newPinRepeat)) { + newPinRepeatView.setError(newPinRepeatView.getContext().getString(R.string.token_error_pin_repeat)); + return; + } + + dialog.dismiss(); + presenter.onInputAdminPin(adminPin, newPin); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenContract.java index ba6f68f02..67084c493 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenContract.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenContract.java @@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.ui.token; import android.net.Uri; import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo; import org.sufficientlysecure.keychain.ui.token.ManageSecurityTokenFragment.StatusLine; @@ -31,7 +32,7 @@ class ManageSecurityTokenContract { void onClickRetry(); void onClickViewKey(); - void onClickViewLog(); + void onMenuClickViewLog(); void onClickImport(); void onImportSuccess(OperationResult result); @@ -41,6 +42,10 @@ class ManageSecurityTokenContract { void onPromoteError(OperationResult result); + void onSecurityTokenChangePinSuccess(SecurityTokenInfo tokenInfo); + + void onSecurityTokenChangePinCanceled(SecurityTokenInfo tokenInfo); + void onClickLoadFile(); void onFileSelected(Uri fileUri); void onStoragePermissionGranted(); @@ -48,9 +53,14 @@ class ManageSecurityTokenContract { void onClickResetToken(); void onClickConfirmReset(); - void onSecurityTokenResetSuccess(); + void onSecurityTokenResetSuccess(SecurityTokenInfo tokenInfo); + void onSecurityTokenResetCanceled(SecurityTokenInfo tokenInfo); void onClickUnlockToken(); + void onMenuClickChangePin(); + void onInputAdminPin(String adminPin, String newPin); + + void onClickUnlockTokenImpossible(); } interface ManageSecurityTokenMvpView { @@ -74,9 +84,12 @@ class ManageSecurityTokenContract { void showFileSelectDialog(); void showConfirmResetDialog(); + void showAdminPinDialog(); void showDisplayLogActivity(OperationResult result); void requestStoragePermission(); + + void showErrorCannotUnlock(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenFragment.java index 58b693ea6..bfd56b7fe 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenFragment.java @@ -31,6 +31,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v4.app.Fragment; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog.Builder; import android.view.LayoutInflater; import android.view.Menu; @@ -41,8 +42,6 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.TextView; -import org.bouncycastle.util.encoders.Hex; -import org.sufficientlysecure.keychain.BuildConfig; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; @@ -65,7 +64,8 @@ import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.AbstractCal import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; import org.sufficientlysecure.keychain.ui.token.ManageSecurityTokenContract.ManageSecurityTokenMvpPresenter; import org.sufficientlysecure.keychain.ui.token.ManageSecurityTokenContract.ManageSecurityTokenMvpView; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.ui.util.ThemeChanger; import org.sufficientlysecure.keychain.ui.widget.StatusIndicator; import org.sufficientlysecure.keychain.ui.widget.StatusIndicator.Status; @@ -149,7 +149,9 @@ public class ManageSecurityTokenFragment extends Fragment implements ManageSecur view.findViewById(R.id.button_reset_token_2).setOnClickListener(this); view.findViewById(R.id.button_reset_token_3).setOnClickListener(this); view.findViewById(R.id.button_reset_token_4).setOnClickListener(this); + view.findViewById(R.id.button_reset_token_5).setOnClickListener(this); view.findViewById(R.id.button_unlock).setOnClickListener(this); + view.findViewById(R.id.button_unlock_impossible).setOnClickListener(this); view.findViewById(R.id.button_load_file).setOnClickListener(this); setHasOptionsMenu(true); @@ -175,7 +177,11 @@ public class ManageSecurityTokenFragment extends Fragment implements ManageSecur public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.view_log: { - presenter.onClickViewLog(); + presenter.onMenuClickViewLog(); + return true; + } + case R.id.change_pin: { + presenter.onMenuClickChangePin(); return true; } default: { @@ -259,13 +265,14 @@ public class ManageSecurityTokenFragment extends Fragment implements ManageSecur @Override public void showActionLocked(int attemptsLeft) { - actionAnimator.setDisplayedChildId(R.id.token_layout_locked); if (attemptsLeft > 0) { + actionAnimator.setDisplayedChildId(R.id.token_layout_locked); + String unlockAttemptsText = getResources().getQuantityString( R.plurals.token_unlock_attempts, attemptsLeft, attemptsLeft); unlockSubtitle.setText(unlockAttemptsText); } else { - unlockSubtitle.setText(R.string.token_unlock_attempts_none); + actionAnimator.setDisplayedChildId(R.id.token_layout_locked_hard); } } @@ -329,12 +336,23 @@ public class ManageSecurityTokenFragment extends Fragment implements ManageSecur .setPositiveButton(R.string.token_reset_confirm_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); presenter.onClickConfirmReset(); } }).show(); } + @Override + public void showAdminPinDialog() { + AlertDialog adminPinDialog = ChangePinDialogHelper.createAdminPinDialog(getContext(), presenter); + + adminPinDialog.show(); + } + + @Override + public void showErrorCannotUnlock() { + Notify.create(getActivity(), R.string.token_error_locked_indefinitely, Style.ERROR).show(); + } + @Override public void showDisplayLogActivity(OperationResult result) { Intent intent = new Intent(getActivity(), LogDisplayActivity.class); @@ -374,8 +392,22 @@ public class ManageSecurityTokenFragment extends Fragment implements ManageSecur break; } case REQUEST_CODE_RESET: { + SecurityTokenInfo tokenInfo = data == null ? null : + data.getParcelableExtra(SecurityTokenOperationActivity.RESULT_TOKEN_INFO); if (resultCode == Activity.RESULT_OK) { - presenter.onSecurityTokenResetSuccess(); + presenter.onSecurityTokenResetSuccess(tokenInfo); + } else { + presenter.onSecurityTokenResetCanceled(tokenInfo); + } + break; + } + case REQUEST_CODE_CHANGE_PIN: { + SecurityTokenInfo tokenInfo = data == null ? null : + data.getParcelableExtra(SecurityTokenOperationActivity.RESULT_TOKEN_INFO); + if (resultCode == Activity.RESULT_OK) { + presenter.onSecurityTokenChangePinSuccess(tokenInfo); + } else { + presenter.onSecurityTokenChangePinCanceled(tokenInfo); } break; } @@ -407,7 +439,8 @@ public class ManageSecurityTokenFragment extends Fragment implements ManageSecur case R.id.button_reset_token_1: case R.id.button_reset_token_2: case R.id.button_reset_token_3: - case R.id.button_reset_token_4: { + case R.id.button_reset_token_4: + case R.id.button_reset_token_5: { presenter.onClickResetToken(); break; } @@ -416,6 +449,10 @@ public class ManageSecurityTokenFragment extends Fragment implements ManageSecur presenter.onClickUnlockToken(); break; } + case R.id.button_unlock_impossible: { + presenter.onClickUnlockTokenImpossible(); + break; + } } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenPresenter.java index 054e396d9..1af2405fd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenPresenter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/token/ManageSecurityTokenPresenter.java @@ -51,7 +51,8 @@ class ManageSecurityTokenPresenter implements ManageSecurityTokenMvpPresenter { private final Context context; private final LoaderManager loaderManager; - private final SecurityTokenInfo tokenInfo; + + private SecurityTokenInfo tokenInfo; private ManageSecurityTokenMvpView view; @@ -92,11 +93,28 @@ class ManageSecurityTokenPresenter implements ManageSecurityTokenMvpPresenter { continueSearch(); } + private void resetAndContinueSearch() { + checkedKeyStatus = false; + searchedLocally = false; + searchedAtUri = false; + searchedKeyservers = false; + + view.hideAction(); + view.resetStatusLines(); + continueSearch(); + } + private void continueSearch() { if (!checkedKeyStatus) { - view.statusLineAdd(StatusLine.CHECK_KEY); - delayPerformKeyCheck(); - return; + boolean keyIsLocked = tokenInfo.getVerifyRetries() == 0; + if (keyIsLocked) { + // the "checking key status" is fake: we only do it if we already know the key is locked + view.statusLineAdd(StatusLine.CHECK_KEY); + delayPerformKeyCheck(); + return; + } else { + checkedKeyStatus = true; + } } if (!searchedLocally) { @@ -150,6 +168,30 @@ class ManageSecurityTokenPresenter implements ManageSecurityTokenMvpPresenter { view.showAdminPinDialog(); } + @Override + public void onMenuClickChangePin() { + if (!checkedKeyStatus) { + return; + } + + if (tokenInfo.getVerifyAdminRetries() == 0) { + view.showErrorCannotUnlock(); + return; + } + + view.showAdminPinDialog(); + } + + @Override + public void onInputAdminPin(String adminPin, String newPin) { + view.operationChangePinSecurityToken(adminPin, newPin); + } + + @Override + public void onClickUnlockTokenImpossible() { + view.showErrorCannotUnlock(); + } + private LoaderCallbacks loaderCallbacks = new LoaderCallbacks() { @Override public Loader onCreateLoader(int id, Bundle args) { @@ -272,13 +314,7 @@ class ManageSecurityTokenPresenter implements ManageSecurityTokenMvpPresenter { @Override public void onClickRetry() { - searchedLocally = false; - searchedAtUri = false; - searchedKeyservers = false; - - view.hideAction(); - view.resetStatusLines(); - continueSearch(); + resetAndContinueSearch(); } @Override @@ -297,8 +333,31 @@ class ManageSecurityTokenPresenter implements ManageSecurityTokenMvpPresenter { } @Override - public void onSecurityTokenResetSuccess() { - // TODO + public void onSecurityTokenResetSuccess(SecurityTokenInfo tokenInfo) { + this.tokenInfo = tokenInfo; + resetAndContinueSearch(); + } + + @Override + public void onSecurityTokenResetCanceled(SecurityTokenInfo tokenInfo) { + if (tokenInfo != null) { + this.tokenInfo = tokenInfo; + resetAndContinueSearch(); + } + } + + @Override + public void onSecurityTokenChangePinSuccess(SecurityTokenInfo tokenInfo) { + this.tokenInfo = tokenInfo; + resetAndContinueSearch(); + } + + @Override + public void onSecurityTokenChangePinCanceled(SecurityTokenInfo tokenInfo) { + if (tokenInfo != null) { + this.tokenInfo = tokenInfo; + resetAndContinueSearch(); + } } @Override @@ -340,7 +399,7 @@ class ManageSecurityTokenPresenter implements ManageSecurityTokenMvpPresenter { } @Override - public void onClickViewLog() { + public void onMenuClickViewLog() { OperationResult result = new GenericOperationResult(GenericOperationResult.RESULT_OK, log); view.showDisplayLogActivity(result); } diff --git a/OpenKeychain/src/main/res/layout/admin_pin_dialog.xml b/OpenKeychain/src/main/res/layout/admin_pin_dialog.xml new file mode 100644 index 000000000..c2909cf4c --- /dev/null +++ b/OpenKeychain/src/main/res/layout/admin_pin_dialog.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/create_security_token_import_fragment.xml b/OpenKeychain/src/main/res/layout/create_security_token_import_fragment.xml index 35ab29536..6554b8e89 100644 --- a/OpenKeychain/src/main/res/layout/create_security_token_import_fragment.xml +++ b/OpenKeychain/src/main/res/layout/create_security_token_import_fragment.xml @@ -50,7 +50,7 @@ android:inAnimation="@anim/fade_in_delayed" android:outAnimation="@anim/fade_out" android:clipChildren="false" - custom:initialView="04"> + custom:initialView="05"> @@ -265,6 +265,81 @@ + + + + + + + + + + + + + + +