From af7d36c0384b87642c58d33024f3ece91c7ac8e0 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Mon, 4 Sep 2017 23:54:56 +0200 Subject: [PATCH] token-import: first iteration --- .../remote/CryptoInputParcelCacheService.java | 1 + .../securitytoken/SecurityTokenHelper.java | 5 + .../keychain/service/ImportKeyringParcel.java | 1 - .../keychain/ui/CreateKeyActivity.java | 6 +- .../ui/CreateSecurityTokenImportFragment.java | 310 ++++++++++++++++++ .../CreateSecurityTokenImportPresenter.java | 240 ++++++++++++++ ...reateSecurityTokenImportResetFragment.java | 35 +- .../ui/CreateSecurityTokenWaitFragment.java | 4 + .../keychain/ui/PublicKeyRetrievalLoader.java | 227 +++++++++++++ .../ui/ViewKeySecurityTokenFragment.java | 1 - .../ui/base/CryptoOperationHelper.java | 40 ++- .../ui/keyview/loader/IdentityLoader.java | 3 +- .../main/res/drawable-hdpi/ic_bomb_24dp.png | Bin 0 -> 616 bytes .../main/res/drawable-mdpi/ic_bomb_24dp.png | Bin 0 -> 412 bytes .../main/res/drawable-xhdpi/ic_bomb_24dp.png | Bin 0 -> 751 bytes .../main/res/drawable-xxhdpi/ic_bomb_24dp.png | Bin 0 -> 1150 bytes .../res/drawable-xxxhdpi/ic_bomb_24dp.png | Bin 0 -> 1424 bytes .../create_security_token_import_fragment.xml | 197 +++++++++++ ...e_security_token_import_reset_fragment.xml | 18 +- .../main/res/layout/status_indicator_line.xml | 25 ++ .../src/main/res/menu/token_setup.xml | 9 + .../src/main/res/values-de/strings.xml | 4 +- .../src/main/res/values-es/strings.xml | 4 +- .../src/main/res/values-eu/strings.xml | 2 +- .../src/main/res/values-fr/strings.xml | 4 +- .../src/main/res/values-ja/strings.xml | 4 +- .../src/main/res/values-nb/strings.xml | 2 +- .../src/main/res/values-nl/strings.xml | 4 +- .../src/main/res/values-pt-rBR/strings.xml | 2 +- .../src/main/res/values-ru/strings.xml | 4 +- .../src/main/res/values-uk/strings.xml | 4 +- .../src/main/res/values-zh/strings.xml | 2 +- OpenKeychain/src/main/res/values/strings.xml | 14 +- graphics/drawables/ic_bomb.svg | 1 + graphics/update-drawables.sh | 2 +- 35 files changed, 1098 insertions(+), 77 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportPresenter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PublicKeyRetrievalLoader.java create mode 100644 OpenKeychain/src/main/res/drawable-hdpi/ic_bomb_24dp.png create mode 100644 OpenKeychain/src/main/res/drawable-mdpi/ic_bomb_24dp.png create mode 100644 OpenKeychain/src/main/res/drawable-xhdpi/ic_bomb_24dp.png create mode 100644 OpenKeychain/src/main/res/drawable-xxhdpi/ic_bomb_24dp.png create mode 100644 OpenKeychain/src/main/res/drawable-xxxhdpi/ic_bomb_24dp.png create mode 100644 OpenKeychain/src/main/res/layout/create_security_token_import_fragment.xml create mode 100644 OpenKeychain/src/main/res/layout/status_indicator_line.xml create mode 100644 OpenKeychain/src/main/res/menu/token_setup.xml create mode 100644 graphics/drawables/ic_bomb.svg diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/CryptoInputParcelCacheService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/CryptoInputParcelCacheService.java index c573aba9b..2a0b3c1fc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/CryptoInputParcelCacheService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/CryptoInputParcelCacheService.java @@ -76,6 +76,7 @@ public class CryptoInputParcelCacheService extends Service { data.setExtrasClassLoader(CryptoInputParcelCacheService.class.getClassLoader()); // And write out the UUID most and least significant bits. + data.setExtrasClassLoader(CryptoInputParcelCacheService.class.getClassLoader()); data.putExtra(OpenPgpApi.EXTRA_CALL_UUID1, mTicket.getMostSignificantBits()); data.putExtra(OpenPgpApi.EXTRA_CALL_UUID2, mTicket.getLeastSignificantBits()); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index 5b687f488..61ed683e7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -587,6 +587,11 @@ public class SecurityTokenHelper { return getData(0x00, 0x4F); } + public String getUrl() throws IOException { + byte[] data = getData(0x5F, 0x50); + return new String(data); + } + public String getUserId() throws IOException { return getHolderName(getData(0x00, 0x65)); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java index e7a0e2d51..7cf0bc2c7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java @@ -19,7 +19,6 @@ package org.sufficientlysecure.keychain.service; -import java.util.ArrayList; import java.util.Collections; import java.util.List; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java index e64d846f6..11ce6b663 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java @@ -77,6 +77,7 @@ public class CreateKeyActivity extends BaseSecurityTokenActivity { byte[] mScannedFingerprints; byte[] mSecurityTokenAid; String mSecurityTokenUserId; + private String mSecurityTokenUrl; @Override public void onCreate(Bundle savedInstanceState) { @@ -162,6 +163,7 @@ public class CreateKeyActivity extends BaseSecurityTokenActivity { mScannedFingerprints = mSecurityTokenHelper.getFingerprints(); mSecurityTokenAid = mSecurityTokenHelper.getAid(); mSecurityTokenUserId = mSecurityTokenHelper.getUserId(); + mSecurityTokenUrl = mSecurityTokenHelper.getUrl(); } @Override @@ -194,8 +196,8 @@ public class CreateKeyActivity extends BaseSecurityTokenActivity { finish(); } catch (PgpKeyNotFoundException e) { - Fragment frag = CreateSecurityTokenImportResetFragment.newInstance( - mScannedFingerprints, mSecurityTokenAid, mSecurityTokenUserId); + Fragment frag = CreateSecurityTokenImportFragment.newInstance( + mScannedFingerprints, mSecurityTokenAid, mSecurityTokenUserId, mSecurityTokenUrl); loadFragment(frag, FragAction.TO_RIGHT); } } else { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportFragment.java new file mode 100644 index 000000000..7e15a174b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportFragment.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.bouncycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.operations.results.PromoteKeyResult; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.service.PromoteKeyringParcel; +import org.sufficientlysecure.keychain.ui.CreateSecurityTokenImportPresenter.CreateSecurityTokenImportMvpView; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.AbstractCallback; +import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.ui.widget.StatusIndicator; +import org.sufficientlysecure.keychain.ui.widget.StatusIndicator.Status; +import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator; + + +public class CreateSecurityTokenImportFragment extends Fragment implements CreateSecurityTokenImportMvpView, + OnClickListener { + private static final String ARG_FINGERPRINTS = "fingerprint"; + private static final String ARG_AID = "aid"; + private static final String ARG_USER_ID = "user_ids"; + private static final String ARG_URL = "key_uri"; + + CreateSecurityTokenImportPresenter presenter; + private ViewGroup statusLayoutGroup; + private ToolableViewAnimator actionAnimator; + + ImportKeyringParcel currentImportKeyringParcel; + PromoteKeyringParcel currentPromoteKeyringParcel; + private LayoutInflater layoutInflater; + private StatusIndicator latestStatusIndicator; + + public static Fragment newInstanceForDebug() { +// byte[] scannedFps = KeyFormattingUtils.convertFingerprintHexFingerprint("4700BA1AC417ABEF3CC7765AD686905837779C3E"); + byte[] scannedFps = KeyFormattingUtils.convertFingerprintHexFingerprint("1efdb4845ca242ca6977fddb1f788094fd3b430a"); + return newInstance(scannedFps, Hex.decode("010203040506"), "yubinu2@mugenguild.com", "http://valodim.stratum0.net/mryubinu3.asc"); + } + + public static Fragment newInstance(byte[] scannedFingerprints, byte[] nfcAid, String userId, String tokenUrl) { + CreateSecurityTokenImportFragment frag = new CreateSecurityTokenImportFragment(); + + Bundle args = new Bundle(); + args.putByteArray(ARG_FINGERPRINTS, scannedFingerprints); + args.putByteArray(ARG_AID, nfcAid); + args.putString(ARG_USER_ID, userId); + args.putString(ARG_URL, tokenUrl); + frag.setArguments(args); + + return frag; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + + byte[] tokenFingerprints = args.getByteArray(ARG_FINGERPRINTS); + byte[] tokenAid = args.getByteArray(ARG_AID); + String tokenUserId = args.getString(ARG_USER_ID); + String tokenUrl = args.getString(ARG_URL); + + presenter = new CreateSecurityTokenImportPresenter( + getContext(), tokenFingerprints, tokenAid, tokenUserId, tokenUrl, getLoaderManager()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + this.layoutInflater = inflater; + View view = inflater.inflate(R.layout.create_security_token_import_fragment, container, false); + + statusLayoutGroup = (ViewGroup) view.findViewById(R.id.status_indicator_layout); + actionAnimator = (ToolableViewAnimator) view.findViewById(R.id.action_animator); + + view.findViewById(R.id.button_import).setOnClickListener(this); + view.findViewById(R.id.button_view_key).setOnClickListener(this); + view.findViewById(R.id.button_retry).setOnClickListener(this); + view.findViewById(R.id.button_reset_token_1).setOnClickListener(this); + view.findViewById(R.id.button_reset_token_2).setOnClickListener(this); + view.findViewById(R.id.button_reset_token_3).setOnClickListener(this); + + setHasOptionsMenu(true); + + presenter.setView(this); + + return view; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.token_setup, menu); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + presenter.onActivityCreated(); + } + + @Override + public void finishAndShowKey(long masterKeyId) { + Activity activity = getActivity(); + + Intent viewKeyIntent = new Intent(activity, ViewKeyActivity.class); + // use the imported masterKeyId, not the one from the token, because + // that one might* just have been a subkey of the imported key + viewKeyIntent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); + + if (activity instanceof CreateKeyActivity) { + ((CreateKeyActivity) activity).finishWithFirstTimeHandling(viewKeyIntent); + } else { + activity.startActivity(viewKeyIntent); + activity.finish(); + } + } + + @Override + public void statusLineAdd(StatusLine statusLine) { + if (latestStatusIndicator != null) { + throw new IllegalStateException("Cannot set next status line before completing previous!"); + } + + View line = layoutInflater.inflate(R.layout.status_indicator_line, statusLayoutGroup, false); + + latestStatusIndicator = (StatusIndicator) line.findViewById(R.id.status_indicator); + latestStatusIndicator.setDisplayedChild(Status.PROGRESS); + TextView latestStatusText = (TextView) line.findViewById(R.id.status_text); + latestStatusText.setText(statusLine.stringRes); + + statusLayoutGroup.addView(line); + } + + @Override + public void statusLineOk() { + latestStatusIndicator.setDisplayedChild(Status.OK); + latestStatusIndicator = null; + } + + @Override + public void statusLineError() { + latestStatusIndicator.setDisplayedChild(Status.ERROR); + latestStatusIndicator = null; + } + + @Override + public void resetStatusLines() { + latestStatusIndicator = null; + statusLayoutGroup.removeAllViews(); + } + + @Override + public void showActionImport() { + actionAnimator.setDisplayedChildId(R.id.token_layout_import); + } + + @Override + public void showActionViewKey() { + actionAnimator.setDisplayedChildId(R.id.token_layout_ok); + } + + @Override + public void showActionRetryOrFromFile() { + actionAnimator.setDisplayedChildId(R.id.token_layout_not_found); + } + + @Override + public void hideAction() { + actionAnimator.setDisplayedChild(0); + } + + @Override + public void operationImportKey(byte[] importKeyData) { + if (currentImportKeyringParcel != null) { + throw new IllegalStateException("Cannot trigger import operation twice!"); + } + + currentImportKeyringParcel = + ImportKeyringParcel.createImportKeyringParcel(ParcelableKeyRing.createFromEncodedBytes(importKeyData)); + cryptoImportOperationHelper.setOperationMinimumDelay(1000L); + cryptoImportOperationHelper.cryptoOperation(); + } + + @Override + public void operationPromote(long masterKeyId, byte[] cardAid) { + if (currentImportKeyringParcel != null) { + throw new IllegalStateException("Cannot trigger import operation twice!"); + } + + currentPromoteKeyringParcel = PromoteKeyringParcel.createPromoteKeyringParcel(masterKeyId, cardAid, null); + cryptoPromoteOperationHelper.setOperationMinimumDelay(1000L); + cryptoPromoteOperationHelper.cryptoOperation(); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.button_import: { + presenter.onClickImport(); + break; + } + case R.id.button_retry: { + presenter.onClickRetry(); + break; + } + case R.id.button_view_key: { + presenter.onClickViewKey(); + break; + } + case R.id.button_reset_token_1: + case R.id.button_reset_token_2: + case R.id.button_reset_token_3: { + presenter.onClickResetToken(); + break; + } + } + } + + CryptoOperationHelper cryptoImportOperationHelper = + new CryptoOperationHelper<>(0, this, new AbstractCallback() { + @Override + public ImportKeyringParcel createOperationInput() { + return currentImportKeyringParcel; + } + + @Override + public void onCryptoOperationSuccess(ImportKeyResult result) { + currentImportKeyringParcel = null; + presenter.onImportSuccess(); + } + + @Override + public void onCryptoOperationError(ImportKeyResult result) { + currentImportKeyringParcel = null; + presenter.onImportError(); + } + }, null); + + CryptoOperationHelper cryptoPromoteOperationHelper = + new CryptoOperationHelper<>(1, this, new AbstractCallback() { + @Override + public PromoteKeyringParcel createOperationInput() { + return currentPromoteKeyringParcel; + } + + @Override + public void onCryptoOperationSuccess(PromoteKeyResult result) { + currentPromoteKeyringParcel = null; + presenter.onPromoteSuccess(); + } + + @Override + public void onCryptoOperationError(PromoteKeyResult result) { + currentPromoteKeyringParcel = null; + presenter.onPromoteError(); + } + }, null); + + enum StatusLine { + SEARCH_LOCAL (R.string.status_search_local), + SEARCH_URI (R.string.status_search_uri), + SEARCH_KEYSERVER (R.string.status_search_keyserver), + IMPORT (R.string.status_import), + TOKEN_PROMOTE(R.string.status_token_promote), + TOKEN_CHECK (R.string.status_token_check); + + @StringRes + private int stringRes; + + StatusLine(@StringRes int stringRes) { + this.stringRes = stringRes; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportPresenter.java new file mode 100644 index 000000000..917e1c07b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportPresenter.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.Loader; + +import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.ui.CreateSecurityTokenImportFragment.StatusLine; +import org.sufficientlysecure.keychain.ui.PublicKeyRetrievalLoader.KeyRetrievalResult; +import org.sufficientlysecure.keychain.ui.PublicKeyRetrievalLoader.KeyserverRetrievalLoader; +import org.sufficientlysecure.keychain.ui.PublicKeyRetrievalLoader.LocalKeyLookupLoader; +import org.sufficientlysecure.keychain.ui.PublicKeyRetrievalLoader.UriKeyRetrievalLoader; + + +class CreateSecurityTokenImportPresenter { + private static final int LOADER_LOCAL = 0; + private static final int LOADER_URI = 1; + private static final int LOADER_KEYSERVER = 2; + + private Context context; + + private byte[][] tokenFingerprints; + private byte[] tokenAid; + private double tokenVersion; + private String tokenUserId; + private final String tokenUrl; + + private LoaderManager loaderManager; + private CreateSecurityTokenImportMvpView view; + private boolean searchedLocally; + private boolean searchedAtUri; + private boolean searchedKeyservers; + + private byte[] importKeyData; + private Long masterKeyId; + + + public CreateSecurityTokenImportPresenter(Context context, byte[] tokenFingerprints, byte[] tokenAid, + String tokenUserId, String tokenUrl, LoaderManager loaderManager) { + this.context = context.getApplicationContext(); + + this.tokenAid = tokenAid; + this.tokenUserId = tokenUserId; + this.tokenUrl = tokenUrl; + this.loaderManager = loaderManager; + + if (tokenFingerprints.length % 20 != 0) { + throw new IllegalArgumentException("fingerprints must be multiple of 20 bytes!"); + } + this.tokenFingerprints = new byte[tokenFingerprints.length / 20][]; + for (int i = 0; i < tokenFingerprints.length / 20; i++) { + this.tokenFingerprints[i] = new byte[20]; + System.arraycopy(tokenFingerprints, i*20, this.tokenFingerprints[i], 0, 20); + } + } + + public void setView(CreateSecurityTokenImportMvpView view) { + this.view = view; + } + + public void onActivityCreated() { + continueSearch(); + } + + private void continueSearchAfterError() { + view.statusLineError(); + continueSearch(); + } + + private void continueSearch() { + if (!searchedLocally) { + view.statusLineAdd(StatusLine.SEARCH_LOCAL); + loaderManager.restartLoader(LOADER_LOCAL, null, loaderCallbacks); + return; + } + + if (!searchedAtUri) { + view.statusLineAdd(StatusLine.SEARCH_URI); + loaderManager.restartLoader(LOADER_URI, null, loaderCallbacks); + return; + } + + if (!searchedKeyservers) { + view.statusLineAdd(StatusLine.SEARCH_KEYSERVER); + loaderManager.restartLoader(LOADER_KEYSERVER, null, loaderCallbacks); + return; + } + + view.showActionRetryOrFromFile(); + } + + private LoaderCallbacks loaderCallbacks = new LoaderCallbacks() { + @Override + public Loader onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_LOCAL: + return new LocalKeyLookupLoader(context, tokenFingerprints); + case LOADER_URI: + return new UriKeyRetrievalLoader(context, tokenUrl, tokenFingerprints); + case LOADER_KEYSERVER: + return new KeyserverRetrievalLoader(context, tokenFingerprints[0]); + } + throw new IllegalArgumentException("called with unknown loader id!"); + } + + @Override + public void onLoadFinished(Loader loader, KeyRetrievalResult data) { + switch (loader.getId()) { + case LOADER_LOCAL: { + searchedLocally = true; + break; + } + case LOADER_URI: { + searchedAtUri = true; + break; + } + case LOADER_KEYSERVER: { + searchedKeyservers = true; + break; + } + default: { + throw new IllegalArgumentException("called with unknown loader id!"); + } + } + + if (data.isSuccess()) { + processResult(data); + } else { + continueSearchAfterError(); + } + } + + @Override + public void onLoaderReset(Loader loader) { + + } + }; + + private void processResult(KeyRetrievalResult result) { + view.statusLineOk(); + + byte[] importKeyData = result.getKeyData(); + Long masterKeyId = result.getMasterKeyId(); + if (importKeyData != null && masterKeyId != null) { + view.showActionImport(); + this.importKeyData = importKeyData; + this.masterKeyId = masterKeyId; + return; + } + + if (masterKeyId != null) { + this.masterKeyId = masterKeyId; + view.statusLineAdd(StatusLine.TOKEN_CHECK); + view.operationPromote(masterKeyId, tokenAid); + return; + } + + throw new IllegalArgumentException("Method can only be called with successful result!"); + } + + void onClickImport() { + view.statusLineAdd(StatusLine.IMPORT); + view.hideAction(); + view.operationImportKey(importKeyData); + } + + void onImportSuccess() { + view.statusLineOk(); + view.statusLineAdd(StatusLine.TOKEN_PROMOTE); + view.operationPromote(masterKeyId, tokenAid); + } + + void onImportError() { + view.statusLineError(); + } + + void onPromoteSuccess() { + view.statusLineOk(); + view.showActionViewKey(); + } + + void onPromoteError() { + view.statusLineError(); + } + + void onClickRetry() { + searchedLocally = false; + searchedAtUri = false; + searchedKeyservers = false; + + view.hideAction(); + view.resetStatusLines(); + continueSearch(); + } + + void onClickViewKey() { + view.finishAndShowKey(masterKeyId); + } + + public void onClickResetToken() { + + } + + interface CreateSecurityTokenImportMvpView { + void statusLineAdd(StatusLine statusLine); + void statusLineOk(); + void statusLineError(); + void resetStatusLines(); + + void showActionImport(); + void showActionViewKey(); + void showActionRetryOrFromFile(); + void hideAction(); + + void operationImportKey(byte[] importKeyData); + void operationPromote(long masterKeyId, byte[] cardAid); + + void finishAndShowKey(long masterKeyId); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java index 41cf0786c..2822bbe6a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java @@ -44,7 +44,6 @@ import org.sufficientlysecure.keychain.ui.CreateKeyActivity.SecurityTokenListene import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment; import org.sufficientlysecure.keychain.ui.keyview.ViewKeyActivity; import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; -import org.sufficientlysecure.keychain.util.Preferences; import java.io.IOException; import java.nio.ByteBuffer; @@ -72,7 +71,6 @@ public class CreateSecurityTokenImportResetFragment private TextView vUserId; private TextView mNextButton; private RadioButton mRadioImport; - private RadioButton mRadioFile; private RadioButton mRadioReset; private View mResetWarning; @@ -117,7 +115,6 @@ public class CreateSecurityTokenImportResetFragment vUserId = (TextView) view.findViewById(R.id.token_userid); mNextButton = (TextView) view.findViewById(R.id.create_key_next_button); mRadioImport = (RadioButton) view.findViewById(R.id.token_decision_import); - mRadioFile = (RadioButton) view.findViewById(R.id.token_decision_file); mRadioReset = (RadioButton) view.findViewById(R.id.token_decision_reset); mResetWarning = view.findViewById(R.id.token_import_reset_warning); @@ -141,8 +138,6 @@ public class CreateSecurityTokenImportResetFragment resetCard(); } else if (mRadioImport.isChecked()){ importKey(); - } else { - importFile(); } } }); @@ -158,17 +153,6 @@ public class CreateSecurityTokenImportResetFragment } } }); - mRadioFile.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - mNextButton.setText(R.string.key_list_fab_import); - mNextButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_folder_grey_24dp, 0); - mNextButton.setVisibility(View.VISIBLE); - mResetWarning.setVisibility(View.GONE); - } - } - }); mRadioReset.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @@ -183,7 +167,6 @@ public class CreateSecurityTokenImportResetFragment setData(); - return view; } @@ -214,21 +197,9 @@ public class CreateSecurityTokenImportResetFragment } public void importKey() { - ArrayList keyList = new ArrayList<>(); - keyList.add(ParcelableKeyRing.createFromReference(mTokenFingerprint, null, null, null)); - mKeyList = keyList; - - mKeyserver = Preferences.getPreferences(getActivity()).getPreferredKeyserver(); - - super.setProgressMessageResource(R.string.progress_importing); - - super.cryptoOperation(); - } - - public void importFile() { - Intent intent = new Intent(getActivity(), ImportKeysActivity.class); - intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE); - startActivity(intent); +// Fragment frag = CreateSecurityTokenImportFragment.newInstance(mTokenFingerprints, mTokenAid, mTokenUserId, +// keyUdi); +// mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); } public void resetCard() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java index 782502741..bd1b47dec 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java @@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui; import android.content.Context; import android.os.Bundle; +import android.os.Handler; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.view.LayoutInflater; @@ -26,9 +27,12 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; +import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; + public class CreateSecurityTokenWaitFragment extends Fragment { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PublicKeyRetrievalLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PublicKeyRetrievalLoader.java new file mode 100644 index 000000000..6ffd473d8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PublicKeyRetrievalLoader.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + + +import java.io.IOException; +import java.util.Arrays; + +import android.content.Context; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.support.v4.content.AsyncTaskLoader; +import android.util.Log; + +import com.google.auto.value.AutoValue; +import okhttp3.Request.Builder; +import okhttp3.Response; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress; +import org.sufficientlysecure.keychain.keyimport.HkpKeyserverClient; +import org.sufficientlysecure.keychain.keyimport.KeyserverClient.QueryFailedException; +import org.sufficientlysecure.keychain.network.OkHttpClientFactory; +import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; +import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; +import org.sufficientlysecure.keychain.ui.PublicKeyRetrievalLoader.KeyRetrievalResult; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.ParcelableProxy; +import org.sufficientlysecure.keychain.util.Preferences; + + +public abstract class PublicKeyRetrievalLoader extends AsyncTaskLoader { + private static final long MIN_OPERATION_TIME_MILLIS = 1500; + + + private KeyRetrievalResult cachedResult; + + + public PublicKeyRetrievalLoader(Context context) { + super(context); + } + + @Override + protected KeyRetrievalResult onLoadInBackground() { + long startTime = SystemClock.elapsedRealtime(); + + KeyRetrievalResult keyRetrievalResult = super.onLoadInBackground(); + + try { + long elapsedTime = startTime - SystemClock.elapsedRealtime(); + if (elapsedTime < MIN_OPERATION_TIME_MILLIS) { + Thread.sleep(MIN_OPERATION_TIME_MILLIS - elapsedTime); + } + } catch (InterruptedException e) { + // nvm + } + + return keyRetrievalResult; + } + + public static class LocalKeyLookupLoader extends PublicKeyRetrievalLoader { + private final KeyRepository keyRepository; + private final byte[][] fingerprints; + + public LocalKeyLookupLoader(Context context, byte[][] fingerprints) { + super(context); + + this.fingerprints = fingerprints; + this.keyRepository = KeyRepository.createDatabaseInteractor(context); + } + + @Override + public KeyRetrievalResult loadInBackground() { + try { + // TODO check other fingerprints + long masterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(fingerprints[0]); + CachedPublicKeyRing cachedPublicKeyRing = keyRepository.getCachedPublicKeyRing(masterKeyId); + switch (cachedPublicKeyRing.getSecretKeyType(masterKeyId)) { + case PASSPHRASE: + case PASSPHRASE_EMPTY: { + return KeyRetrievalResult.createWithMasterKeyIdAndSecretAvailable(masterKeyId); + } + + case GNU_DUMMY: + case DIVERT_TO_CARD: + case UNAVAILABLE: { + return KeyRetrievalResult.createWithMasterKeyId(masterKeyId); + } + + default: { + throw new IllegalStateException("Unhandled SecretKeyType!"); + } + } + } catch (NotFoundException e) { + return KeyRetrievalResult.createWithError(); + } + } + } + + public static class UriKeyRetrievalLoader extends PublicKeyRetrievalLoader { + byte[][] fingerprints; + String yubikeyUri; + + public UriKeyRetrievalLoader(Context context, String yubikeyUri, byte[][] fingerprints) { + super(context); + + this.yubikeyUri = yubikeyUri; + this.fingerprints = fingerprints; + } + + @Override + public KeyRetrievalResult loadInBackground() { + try { + Response execute = + OkHttpClientFactory.getSimpleClient().newCall(new Builder().url(yubikeyUri).build()).execute(); + if (execute.isSuccessful()) { + UncachedKeyRing keyRing = UncachedKeyRing.decodeFromData(execute.body().bytes()); + if (Arrays.equals(fingerprints[0], keyRing.getFingerprint())) { + return KeyRetrievalResult.createWithKeyringdata(keyRing.getMasterKeyId(), keyRing.getEncoded()); + } + } + } catch (IOException | PgpGeneralException e) { + Log.e(Constants.TAG, "error retrieving key from uri", e); + } + + return KeyRetrievalResult.createWithError(); + } + } + + public static class KeyserverRetrievalLoader extends PublicKeyRetrievalLoader { + byte[] fingerprint; + + public KeyserverRetrievalLoader(Context context, byte[] fingerprint) { + super(context); + + this.fingerprint = fingerprint; + } + + @Override + public KeyRetrievalResult loadInBackground() { + HkpKeyserverAddress preferredKeyserver = Preferences.getPreferences(getContext()).getPreferredKeyserver(); + ParcelableProxy parcelableProxy = Preferences.getPreferences(getContext()).getParcelableProxy(); + + HkpKeyserverClient keyserverClient = HkpKeyserverClient.fromHkpKeyserverAddress(preferredKeyserver); + + try { + String keyString = + keyserverClient.get("0x" + KeyFormattingUtils.convertFingerprintToHex(fingerprint), parcelableProxy); + UncachedKeyRing keyRing = UncachedKeyRing.decodeFromData(keyString.getBytes()); + + return KeyRetrievalResult.createWithKeyringdata(keyRing.getMasterKeyId(), keyRing.getEncoded()); + } catch (QueryFailedException | IOException | PgpGeneralException e) { + Log.e(Constants.TAG, "error retrieving key from keyserver", e); + } + + return KeyRetrievalResult.createWithError(); + } + } + + @Override + public void deliverResult(KeyRetrievalResult result) { + cachedResult = result; + + if (isStarted()) { + super.deliverResult(result); + } + } + + @Override + protected void onStartLoading() { + if (cachedResult != null) { + deliverResult(cachedResult); + } + + if (takeContentChanged() || cachedResult == null) { + forceLoad(); + } + } + + @AutoValue + static abstract class KeyRetrievalResult { + @Nullable + abstract Long getMasterKeyId(); + @Nullable + abstract byte[] getKeyData(); + abstract boolean isSecretKeyAvailable(); + + boolean isSuccess() { + return getMasterKeyId() != null || getKeyData() != null; + } + + static KeyRetrievalResult createWithError() { + return new AutoValue_PublicKeyRetrievalLoader_KeyRetrievalResult(null, null, false); + } + + static KeyRetrievalResult createWithKeyringdata(long masterKeyId, byte[] keyringData) { + return new AutoValue_PublicKeyRetrievalLoader_KeyRetrievalResult(masterKeyId, keyringData, false); + } + + static KeyRetrievalResult createWithMasterKeyIdAndSecretAvailable(long masterKeyId) { + return new AutoValue_PublicKeyRetrievalLoader_KeyRetrievalResult(masterKeyId, null, true); + } + + static KeyRetrievalResult createWithMasterKeyId(long masterKeyId) { + return new AutoValue_PublicKeyRetrievalLoader_KeyRetrievalResult(masterKeyId, null, false); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeySecurityTokenFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeySecurityTokenFragment.java index bbea3973b..b3fe73464 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeySecurityTokenFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeySecurityTokenFragment.java @@ -99,7 +99,6 @@ public class ViewKeySecurityTokenFragment mMasterKeyId = args.getLong(ARG_MASTER_KEY_ID); getLoaderManager().initLoader(0, null, this); - } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java index f7316d811..b2c8fd853 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java @@ -25,9 +25,11 @@ import android.app.Activity; import android.app.ProgressDialog; import android.content.Intent; import android.os.Bundle; +import android.os.Handler; import android.os.Message; import android.os.Messenger; import android.os.Parcelable; +import android.os.SystemClock; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; @@ -55,6 +57,8 @@ import org.sufficientlysecure.keychain.util.Log; */ public class CryptoOperationHelper { + private long operationStartTime; + public interface Callback { T createOperationInput(); @@ -67,6 +71,19 @@ public class CryptoOperationHelper + implements Callback { + @Override + public void onCryptoOperationCancelled() { + throw new UnsupportedOperationException("Unexpectedly cancelled operation!!"); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + } + // request codes from CryptoOperationHelper are created essentially // a static property, used to identify requestCodes meant for this // particular helper. a request code looks as follows: @@ -85,6 +102,7 @@ public class CryptoOperationHelper minimumOperationDelay) { + returnResultToCallback(result); + return; + } + + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + returnResultToCallback(result); + } + }, minimumOperationDelay - elapsedTime); + } + + private void returnResultToCallback(OperationResult result) { try { if (result.success()) { // noinspection unchecked, because type erasure :( diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java index a14287811..a6b8a137c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java @@ -151,7 +151,8 @@ public class IdentityLoader extends AsyncTaskLoader> { private Intent getTrustIdActivityIntentIfResolvable(String packageName, String autocryptPeer) { Intent intent = new Intent(); - intent.setAction(packageName + ".AUTOCRYPT_PEER_ACTION"); + intent.setAction("org.autocrypt.PEER_ACTION"); + intent.setPackage(packageName); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID, autocryptPeer); diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_bomb_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_bomb_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0f05efecb2780bb3e42aaa8e9978ec72c4d74157 GIT binary patch literal 616 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 Q5&!@I07*qoM6N<$f+5We@Bjb+ literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_bomb_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_bomb_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1e2a035c1c953249510b63c535f57f376797d9d4 GIT binary patch literal 1424 zcmV;B1#kL^P)k8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H11r13=K~#90?VZby6-5+>KLmzxU?O2ApgtJ$=BXhVJE*2Y@e7^>h(C0z6#u9(f<#7zKU- z)|9+YP%H$-fU6W2zin564>KP{C=-B}fVu3vU%6fqqhc9wnDWBs?GSKtf%_E=!2iW> z+fTrvGMV$8g(%_bir==cQ1!KmA^{i$F6Yjh170bgMZX!tp7?G1pk|snJ^6Fl@u=90 zxz3*kb^x1z#hHx&Z)X2zdMDgpF->bwt)1w_2Xoc44;VCDI|%GITz{}ey0!sVh+O=8 zFpFQLdte^sAgr&EzHO+I5T*D4Z=kPsOizBEAz()1YkSQ!M&0iJoX(={sNid-#@7ME zHM&yMGn}Ne02?d#8qxUr(Qr-A2Vj=Y0xYTEYf$5B)^JTv1)y%bz6iR2-rnKdRhn}z zR7rp8j#iw}9DljPF=OcKUC;=a7uKfpi9ax#!_Uu@vbH^*D-+P(}z~dI~^JvBN zdruPqfO6vqdHb?@_WYzDcmeY-)uOf^cs}!KVjXb4g8y^C-L-S<3AX`zfV0#WK5sL? zJHV}m`)@%BT1NaW7V_v)RAL(Z6tmq91sV=7rDO$H0JaUcKWE;5hEZZ`=2%c79KAFK()wZgv{j zAee72SP%Sxd$DykO~gT1jp{FPEpj)TLUp)BML+N@?!_%?-x1l1S=ZU+v*|k5f@3T0 z#jI+k-w8pmgfQxTolT+U6Nxcj(mC)J@TBlPBpE_Y)H>`sAyrsGGkrVK)&g#^tj&K& z4oSX-aPe)3#lWks6H%5v^b?*!w6h3JE0-z(TM;a_4p@Oui%2Q}FADi}$*6==iFLw# zz@Ljtz)1mGb7pT6**ZHE#8s4_t|6h>@2lnB6D z0iV8Lea^o`0PYm<=?iz+b|?{mr2;;E!Ft{#0ua?eMh@#;D-r>?Cg9T-tmjQ404)KZ zzF=B4QwfOxTomx>3qb_ncLATiFlpPNL;y^y3q!|x-XsEWLcpgl1QCEE0zQ4=YugSb zx}z7M7A_14_;$(PsDYU)Hl0e8fH~Aw0s!oGs0GPo2Q(V`Xz>SUMEHIX%#F5&vY!~X2Cy6ffp?1>R2mD2K(a!^)qN+KO z{`|Ux@F~Ojz|I8%V~+#?0000 + + + + + + + + + + + + + + + + + + + + + + +