/* * Copyright (C) 2017 Schürmann & Breitmoser GbR * * 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.keyview; import java.util.Collections; import java.util.List; import android.arch.lifecycle.LiveData; import android.arch.lifecycle.ViewModel; import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.Intent; import android.graphics.PorterDuff; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager.OnBackStackChangedListener; import android.support.v4.content.ContextCompat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextSwitcher; import android.widget.TextView; import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.Constants.key; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.linked.LinkedAttribute; import org.sufficientlysecure.keychain.linked.LinkedResource; import org.sufficientlysecure.keychain.linked.LinkedTokenResource; import org.sufficientlysecure.keychain.linked.UriAttribute; import org.sufficientlysecure.keychain.daos.CertificationDao; import org.sufficientlysecure.keychain.livedata.GenericLiveData; import org.sufficientlysecure.keychain.model.Certification.CertDetails; import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo; import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.daos.KeyRepository; import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.service.CertifyActionsParcel; import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction; import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter; import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; import org.sufficientlysecure.keychain.ui.keyview.LinkedIdViewFragment.ViewHolder.VerifyState; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityDao.LinkedIdInfo; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker; import org.sufficientlysecure.keychain.ui.widget.CertListWidget; import org.sufficientlysecure.keychain.ui.widget.KeySpinner; import timber.log.Timber; public class LinkedIdViewFragment extends CryptoOperationFragment implements OnBackStackChangedListener { private static final String ARG_LID_RANK = "rank"; private static final String ARG_IS_SECRET = "verified"; private static final String ARG_MASTER_KEY_ID = "master_key_id"; private long masterKeyId; private boolean isSecret; private UriAttribute linkedId; private LinkedTokenResource linkedResource; private AsyncTask taskInProgress; private ViewHolder viewHolder; private int lidRank; private long certifyKeyId; public static LinkedIdViewFragment newInstance(long masterKeyId, int rank, boolean isSecret) { LinkedIdViewFragment frag = new LinkedIdViewFragment(); Bundle args = new Bundle(); args.putInt(ARG_LID_RANK, rank); args.putBoolean(ARG_IS_SECRET, isSecret); args.putLong(ARG_MASTER_KEY_ID, masterKeyId); frag.setArguments(args); return frag; } public LinkedIdViewFragment() { // IMPORTANT: the id must be unique in the ViewKeyActivity CryptoOperationHelper id namespace! // no initial progress message -> we handle progress ourselves! super(5, null); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle args = getArguments(); lidRank = args.getInt(ARG_LID_RANK); isSecret = args.getBoolean(ARG_IS_SECRET); masterKeyId = args.getLong(ARG_MASTER_KEY_ID); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); LinkedIdViewModel viewModel = ViewModelProviders.of(this).get(LinkedIdViewModel.class); viewModel.getLinkedIdInfo(requireContext(), masterKeyId, lidRank).observe(this, this::onLinkedIdInfoLoaded); viewModel.getCertifyingKeys(requireContext()).observe(this, viewHolder.vKeySpinner::setData); } private void onLinkedIdInfoLoaded(LinkedIdInfo linkedIdInfo) { if (linkedIdInfo == null) { Timber.e("error loading identity"); Notify.create(getActivity(), "Error loading linked identity!", Notify.LENGTH_LONG, Style.ERROR).show(); finishFragment(); return; } loadIdentity(linkedIdInfo.getLinkedAttribute(), linkedIdInfo.isVerified()); } public void finishFragment() { new Handler().post(() -> { FragmentManager manager = getFragmentManager(); manager.removeOnBackStackChangedListener(LinkedIdViewFragment.this); manager.popBackStack("linked_id", FragmentManager.POP_BACK_STACK_INCLUSIVE); }); } private void loadIdentity(LinkedAttribute linkedId, boolean isVerified) { this.linkedId = linkedId; LinkedResource res = ((LinkedAttribute) this.linkedId).mResource; linkedResource = (LinkedTokenResource) res; if (!isSecret) { if (isVerified) { KeyFormattingUtils.setStatusImage(getContext(), viewHolder.mLinkedIdHolder.vVerified, null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR); } else { KeyFormattingUtils.setStatusImage(getContext(), viewHolder.mLinkedIdHolder.vVerified, null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR); } } else { viewHolder.mLinkedIdHolder.vVerified.setImageResource(R.drawable.octo_link_24dp); } viewHolder.mLinkedIdHolder.bind(getContext(), this.linkedId); setShowVerifying(false); if (linkedResource.isViewable()) { viewHolder.vButtonView.setVisibility(View.VISIBLE); viewHolder.vButtonView.setOnClickListener(v -> { Intent intent = linkedResource.getViewIntent(); if (intent == null) { return; } startActivity(intent); }); } else { viewHolder.vButtonView.setVisibility(View.GONE); } } static class ViewHolder { private final View vButtonView; private final ViewAnimator vVerifyingContainer; private final ViewAnimator vItemCertified; private final View vKeySpinnerContainer; IdentityAdapter.LinkedIdViewHolder mLinkedIdHolder; private ViewAnimator vButtonSwitcher; private CertListWidget vLinkedCerts; private KeySpinner vKeySpinner; private final View vButtonVerify; private final View vButtonRetry; private final View vButtonConfirm; private final ViewAnimator vProgress; private final TextSwitcher vText; ViewHolder(View root) { vLinkedCerts = root.findViewById(R.id.linked_id_certs); vKeySpinner = root.findViewById(R.id.cert_key_spinner); vKeySpinnerContainer = root.findViewById(R.id.cert_key_spincontainer); vButtonSwitcher = root.findViewById(R.id.button_animator); mLinkedIdHolder = new IdentityAdapter.LinkedIdViewHolder(root, null); vButtonVerify = root.findViewById(R.id.button_verify); vButtonRetry = root.findViewById(R.id.button_retry); vButtonConfirm = root.findViewById(R.id.button_confirm); vButtonView = root.findViewById(R.id.button_view); vVerifyingContainer = root.findViewById(R.id.linked_verify_container); vItemCertified = root.findViewById(R.id.linked_id_certified); vProgress = root.findViewById(R.id.linked_cert_progress); vText = root.findViewById(R.id.linked_cert_text); vKeySpinner.setShowNone(R.string.choice_select_cert); } enum VerifyState { VERIFYING, VERIFY_OK, VERIFY_ERROR, CERTIFYING } void setVerifyingState(Context context, VerifyState state, boolean isSecret) { switch (state) { case VERIFYING: vProgress.setDisplayedChild(0); vText.setText(context.getString(R.string.linked_text_verifying)); vKeySpinnerContainer.setVisibility(View.GONE); break; case VERIFY_OK: vProgress.setDisplayedChild(1); if (!isSecret) { showButton(2); if (!vKeySpinner.isSingleEntry()) { vKeySpinnerContainer.setVisibility(View.VISIBLE); } } else { showButton(1); vKeySpinnerContainer.setVisibility(View.GONE); } break; case VERIFY_ERROR: showButton(1); vProgress.setDisplayedChild(2); vText.setText(context.getString(R.string.linked_text_error)); vKeySpinnerContainer.setVisibility(View.GONE); break; case CERTIFYING: vProgress.setDisplayedChild(0); vText.setText(context.getString(R.string.linked_text_confirming)); vKeySpinnerContainer.setVisibility(View.GONE); vButtonConfirm.setEnabled(false); break; } } void showVerifyingContainer(Context context, boolean show, boolean isSecret) { if (vVerifyingContainer.getDisplayedChild() == (show ? 1 : 0)) { return; } vVerifyingContainer.setInAnimation(context, show ? R.anim.fade_in_up : R.anim.fade_in_down); vVerifyingContainer.setOutAnimation(context, show ? R.anim.fade_out_up : R.anim.fade_out_down); vVerifyingContainer.setDisplayedChild(show ? 1 : 0); vItemCertified.setInAnimation(context, show ? R.anim.fade_in_up : R.anim.fade_in_down); vItemCertified.setOutAnimation(context, show ? R.anim.fade_out_up : R.anim.fade_out_down); vItemCertified.setDisplayedChild(show || isSecret ? 1 : 0); } void showButton(int which) { if (vButtonSwitcher.getDisplayedChild() == which) { return; } vButtonSwitcher.setDisplayedChild(which); } } private boolean mVerificationState = false; /** Switches between the 'verifying' ui bit and certificate status. This method * must behave correctly in all states, showing or hiding the appropriate views * and cancelling pending operations where necessary. * * This method also handles back button functionality in combination with * onBackStateChanged. */ void setShowVerifying(boolean show) { if (!show) { if (taskInProgress != null) { taskInProgress.cancel(false); taskInProgress = null; } getFragmentManager().removeOnBackStackChangedListener(this); new Handler().post(() -> getFragmentManager().popBackStack("verification", FragmentManager.POP_BACK_STACK_INCLUSIVE)); if (!mVerificationState) { return; } mVerificationState = false; viewHolder.showButton(0); viewHolder.vKeySpinnerContainer.setVisibility(View.GONE); viewHolder.showVerifyingContainer(getContext(), false, isSecret); return; } if (mVerificationState) { return; } mVerificationState = true; FragmentManager manager = getFragmentManager(); manager.beginTransaction().addToBackStack("verification").commit(); manager.executePendingTransactions(); manager.addOnBackStackChangedListener(this); viewHolder.showVerifyingContainer(getContext(), true, isSecret); } @Override public void onBackStackChanged() { setShowVerifying(false); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.linked_id_view_fragment, superContainer, false); Context context = getContext(); if (context == null) { throw new NullPointerException(); } viewHolder = new ViewHolder(root); root.setTag(viewHolder); ((ImageView) root.findViewById(R.id.status_icon_verified)) .setColorFilter(ContextCompat.getColor(context, R.color.android_green_light), PorterDuff.Mode.SRC_IN); ((ImageView) root.findViewById(R.id.status_icon_invalid)) .setColorFilter(ContextCompat.getColor(context, R.color.android_red_light), PorterDuff.Mode.SRC_IN); viewHolder.vButtonVerify.setOnClickListener(v -> verifyResource()); viewHolder.vButtonRetry.setOnClickListener(v -> verifyResource()); viewHolder.vButtonConfirm.setOnClickListener(v -> initiateCertifying()); LinkedIdViewModel viewModel = ViewModelProviders.of(this).get(LinkedIdViewModel.class); viewModel.getCertDetails(context, masterKeyId, lidRank).observe(this, this::onLoadCertDetails); return root; } private void onLoadCertDetails(CertDetails certDetails) { viewHolder.vLinkedCerts.setData(certDetails, isSecret); } void verifyResource() { // only one at a time (no sync needed, taskInProgress is only touched in ui thread) if (taskInProgress != null) { return; } setShowVerifying(true); viewHolder.vKeySpinnerContainer.setVisibility(View.GONE); viewHolder.setVerifyingState(getContext(), VerifyState.VERIFYING, isSecret); taskInProgress = new AsyncTask() { @Override protected LinkedVerifyResult doInBackground(Void... params) { FragmentActivity activity = getActivity(); byte[] fingerprint; try { fingerprint = KeyRepository.create(activity).getFingerprintByKeyId(masterKeyId); } catch (NotFoundException e) { throw new IllegalStateException("Key to verify linked id for must exist in db!"); } long timer = System.currentTimeMillis(); LinkedVerifyResult result = linkedResource.verify(activity, fingerprint); // ux flow: this operation should take at last a second timer = System.currentTimeMillis() -timer; if (timer < 1000) try { Thread.sleep(1000 -timer); } catch (InterruptedException e) { // never mind } return result; } @Override protected void onPostExecute(LinkedVerifyResult result) { if (isCancelled()) { return; } if (result.success()) { viewHolder.vText.setText(getString(linkedResource.getVerifiedText(isSecret))); // hack to preserve bold text ((TextView) viewHolder.vText.getCurrentView()).setText( linkedResource.getVerifiedText(isSecret)); viewHolder.setVerifyingState(getContext(), VerifyState.VERIFY_OK, isSecret); viewHolder.mLinkedIdHolder.seekAttention(); } else { viewHolder.setVerifyingState(getContext(), VerifyState.VERIFY_ERROR, isSecret); result.createNotify(getActivity()).show(); } taskInProgress = null; } }.execute(); } private void initiateCertifying() { if (isSecret) { return; } // get the user's passphrase for this key (if required) certifyKeyId = viewHolder.vKeySpinner.getSelectedKeyId(); if (certifyKeyId == key.none || certifyKeyId == key.symmetric) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { SubtleAttentionSeeker.tintBackground(viewHolder.vKeySpinnerContainer, 600).start(); } else { Notify.create(getActivity(), R.string.select_key_to_certify, Style.ERROR).show(); } return; } viewHolder.setVerifyingState(getContext(), VerifyState.CERTIFYING, false); cryptoOperation(); } @Override public void onCryptoOperationCancelled() { super.onCryptoOperationCancelled(); // go back to 'verified ok' setShowVerifying(false); } @Nullable @Override public Parcelable createOperationInput() { CertifyAction action = CertifyAction.createForUserAttributes(masterKeyId, Collections.singletonList(linkedId.toUserAttribute())); // fill values for this action CertifyActionsParcel.Builder builder = CertifyActionsParcel.builder(certifyKeyId); builder.addActions(Collections.singletonList(action)); return builder.build(); } @Override public void onCryptoOperationSuccess(OperationResult result) { result.createNotify(getActivity()).show(); // no need to do anything else, we will get a loader refresh! } @Override public void onCryptoOperationError(OperationResult result) { result.createNotify(getActivity()).show(); } @Override public boolean onCryptoSetProgress(String msg, int progress, int max) { return true; } public static class LinkedIdViewModel extends ViewModel { LiveData> certifyingKeysLiveData; LiveData certDetailsLiveData; LiveData linkedIfInfoLiveData; LiveData> getCertifyingKeys(Context context) { if (certifyingKeysLiveData == null) { certifyingKeysLiveData = new GenericLiveData<>(context, () -> { KeyRepository keyRepository = KeyRepository.create(context); return keyRepository.getAllUnifiedKeyInfoWithSecret(); }); } return certifyingKeysLiveData; } LiveData getCertDetails(Context context, long masterKeyId, int lidRank) { if (certDetailsLiveData == null) { CertificationDao certificationDao = CertificationDao.getInstance(context); certDetailsLiveData = new GenericLiveData<>(context, masterKeyId, () -> certificationDao.getVerifyingCertDetails(masterKeyId, lidRank)); } return certDetailsLiveData; } public LiveData getLinkedIdInfo(Context context, long masterKeyId, int lidRank) { if (linkedIfInfoLiveData == null) { IdentityDao identityDao = IdentityDao.getInstance(context); linkedIfInfoLiveData = new GenericLiveData<>(context, masterKeyId, () -> identityDao.getLinkedIdInfo(masterKeyId, lidRank)); } return linkedIfInfoLiveData; } } }