From 5faeb5f5f060e049000e804deca5445d281f8611 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Mon, 12 Jan 2015 13:17:18 +0100 Subject: intermediate state, nothing really working yet --- OpenKeychain/src/main/AndroidManifest.xml | 4 + .../keychain/pgp/affirmation/Affirmation.java | 157 ++++++++++++ .../pgp/affirmation/AffirmationResource.java | 43 ++++ .../pgp/affirmation/resources/DnsResouce.java | 4 + .../resources/GenericHttpsResource.java | 130 ++++++++++ .../pgp/affirmation/resources/TwitterResource.java | 4 + .../pgp/affirmation/resources/UnknownResource.java | 20 ++ .../keychain/ui/ViewKeyMainFragment.java | 19 ++ .../AffirmationCreateHttpsStep1Fragment.java | 275 +++++++++++++++++++++ .../AffirmationCreateHttpsStep2Fragment.java | 256 +++++++++++++++++++ .../ui/affirmations/AffirmationSelectFragment.java | 69 ++++++ .../ui/affirmations/AffirmationWizard.java | 98 ++++++++ .../keychain/ui/widget/HttpsPrefixedText.java | 38 +++ OpenKeychain/src/main/res/drawable/ssl_lock.png | Bin 0 -> 479 bytes OpenKeychain/src/main/res/drawable/twitter.png | Bin 0 -> 5122 bytes .../affirmation_create_https_fragment_step1.xml | 121 +++++++++ .../affirmation_create_https_fragment_step2.xml | 182 ++++++++++++++ .../res/layout/affirmation_select_fragment.xml | 118 +++++++++ .../src/main/res/layout/view_key_main_fragment.xml | 22 ++ OpenKeychain/src/main/res/values/strings.xml | 10 + 20 files changed, 1570 insertions(+) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/Affirmation.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/AffirmationResource.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/DnsResouce.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/GenericHttpsResource.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/TwitterResource.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/UnknownResource.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationCreateHttpsStep1Fragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationCreateHttpsStep2Fragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationSelectFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationWizard.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/HttpsPrefixedText.java create mode 100644 OpenKeychain/src/main/res/drawable/ssl_lock.png create mode 100644 OpenKeychain/src/main/res/drawable/twitter.png create mode 100644 OpenKeychain/src/main/res/layout/affirmation_create_https_fragment_step1.xml create mode 100644 OpenKeychain/src/main/res/layout/affirmation_create_https_fragment_step2.xml create mode 100644 OpenKeychain/src/main/res/layout/affirmation_select_fragment.xml diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 9fc79e6f1..b38a6137d 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -107,6 +107,10 @@ android:name=".ui.EditKeyActivity" android:configChanges="orientation|screenSize|keyboardHidden|keyboard" android:label="@string/title_edit_key" /> + mFlags; + final HashMap mParams; + + protected Affirmation(byte[] data, long nonce, Set flags, + HashMap params, URI subUri) { + mData = data; + mNonce = nonce; + mFlags = flags; + mParams = params; + mSubUri = subUri; + } + + Affirmation(long nonce, Set flags, + HashMap params, URI subUri) { + this(null, nonce, flags, params, subUri); + } + + public byte[] encode() { + if (mData != null) { + return mData; + } + + StringBuilder b = new StringBuilder(); + b.append("pgpid:"); + + // add flags + if (mFlags != null) { + boolean first = true; + for (String flag : mFlags) { + if (!first) { + b.append(";"); + } + first = false; + b.append(flag); + } + } + + // add parameters + if (mParams != null) { + boolean first = true; + Iterator> it = mParams.entrySet().iterator(); + while (it.hasNext()) { + if (!first) { + b.append(";"); + } + first = false; + Entry entry = it.next(); + b.append(entry.getKey()).append("=").append(entry.getValue()); + } + } + + b.append("@"); + b.append(mSubUri); + + byte[] data = Strings.toUTF8ByteArray(b.toString()); + + byte[] result = new byte[data.length+4]; + result[0] = (byte) (mNonce >> 24 & 255); + result[1] = (byte) (mNonce >> 16 & 255); + result[2] = (byte) (mNonce >> 8 & 255); + result[3] = (byte) (mNonce & 255); + + System.arraycopy(data, 0, result, 4, result.length); + + return result; + } + + /** This method parses an affirmation from a UserAttributeSubpacket, or returns null if the + * subpacket can not be parsed as a valid affirmation. + */ + public static Affirmation parseAffirmation(UserAttributeSubpacket subpacket) { + if (subpacket.getType() != 100) { + return null; + } + + byte[] data = subpacket.getData(); + + long nonce = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]; + + try { + return parseUri(nonce, Strings.fromUTF8ByteArray(Arrays.copyOfRange(data, 4, data.length))); + + } catch (IllegalArgumentException e) { + Log.e(Constants.TAG, "error parsing uri in (suspected) affirmation packet"); + return null; + } + } + + public static Affirmation generateForUri(String uri) { + return parseUri(generateNonce(), uri); + } + + protected static Affirmation parseUri (long nonce, String uriString) { + URI uri = URI.create(uriString); + + if ("pgpid".equals(uri.getScheme())) { + Log.e(Constants.TAG, "unknown uri scheme in (suspected) affirmation packet"); + return null; + } + + if (!uri.isOpaque()) { + Log.e(Constants.TAG, "non-opaque uri in (suspected) affirmation packet"); + return null; + } + + String specific = uri.getSchemeSpecificPart(); + if (!specific.contains("@")) { + Log.e(Constants.TAG, "unknown uri scheme in affirmation packet"); + return null; + } + + String[] pieces = specific.split("@", 2); + URI subUri = URI.create(pieces[1]); + + Set flags = new HashSet(); + HashMap params = new HashMap(); + { + String[] rawParams = pieces[0].split(";"); + for (String param : rawParams) { + String[] p = param.split("=", 2); + if (p.length == 1) { + flags.add(param); + } else { + params.put(p[0], p[1]); + } + } + } + + return new Affirmation(null, nonce, flags, params, subUri); + + } + + public static long generateNonce() { + return 1234567890L; // new SecureRandom().nextLong(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/AffirmationResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/AffirmationResource.java new file mode 100644 index 000000000..e356ccb8e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/AffirmationResource.java @@ -0,0 +1,43 @@ +package org.sufficientlysecure.keychain.pgp.affirmation; + +import org.sufficientlysecure.keychain.pgp.affirmation.resources.GenericHttpsResource; +import org.sufficientlysecure.keychain.pgp.affirmation.resources.UnknownResource; + +import java.net.URI; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Set; + +public abstract class AffirmationResource { + + protected final URI mUri; + protected final Set mFlags; + protected final HashMap mParams; + + protected AffirmationResource(Set flags, HashMap params, URI uri) { + mFlags = flags; + mParams = params; + mUri = uri; + } + + public abstract boolean verify(); + + public static AffirmationResource findResourceType + (Set flags, HashMap params, URI uri) { + + AffirmationResource res; + + res = GenericHttpsResource.create(flags, params, uri); + if (res != null) { + return res; + } + + return new UnknownResource(flags, params, uri); + + } + + public static long generateNonce() { + return 1234567890L; // new SecureRandom().nextLong(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/DnsResouce.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/DnsResouce.java new file mode 100644 index 000000000..3e39a695d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/DnsResouce.java @@ -0,0 +1,4 @@ +package org.sufficientlysecure.keychain.pgp.affirmation.resources; + +public class DnsResouce { +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/GenericHttpsResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/GenericHttpsResource.java new file mode 100644 index 000000000..42615d105 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/GenericHttpsResource.java @@ -0,0 +1,130 @@ +package org.sufficientlysecure.keychain.pgp.affirmation.resources; + +import android.content.Context; + +import com.textuality.keybase.lib.Search; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; +import org.sufficientlysecure.keychain.pgp.Progressable; +import org.sufficientlysecure.keychain.pgp.affirmation.AffirmationResource; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import javax.net.ssl.HttpsURLConnection; + +public class GenericHttpsResource extends AffirmationResource { + + GenericHttpsResource(Set flags, HashMap params, URI uri) { + super(flags, params, uri); + } + + @Override + public boolean verify() { + return false; + } + + public static String generate (byte[] fingerprint, String uri) { + long nonce = generateNonce(); + + StringBuilder b = new StringBuilder(); + b.append("---\r\n"); + + b.append("fingerprint="); + b.append(KeyFormattingUtils.convertFingerprintToHex(fingerprint)); + b.append('\r').append('\n'); + + b.append("nonce="); + b.append(nonce); + b.append('\r').append('\n'); + + if (uri != null) { + b.append("uri="); + b.append(uri); + b.append('\r').append('\n'); + } + b.append("---\r\n"); + + return b.toString(); + } + + public DecryptVerifyResult verify + (Context context, ProviderHelper providerHelper, Progressable progress) + throws IOException { + + byte[] data = fetchResource(mUri).getBytes(); + InputData input = new InputData(new ByteArrayInputStream(data), data.length); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PgpDecryptVerify.Builder b = + new PgpDecryptVerify.Builder(context, providerHelper, progress, input, out); + PgpDecryptVerify op = b.build(); + + Log.d(Constants.TAG, new String(out.toByteArray())); + + return op.execute(); + } + + protected static String fetchResource (URI uri) throws IOException { + + try { + HttpsURLConnection conn = null; + URL url = uri.toURL(); + int status = 0; + int redirects = 0; + while (redirects < 5) { + conn = (HttpsURLConnection) url.openConnection(); + conn.addRequestProperty("User-Agent", "OpenKeychain"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(25000); + conn.connect(); + status = conn.getResponseCode(); + if (status == 301) { + redirects++; + url = new URL(conn.getHeaderFields().get("Location").get(0)); + } else { + break; + } + } + if (status >= 200 && status < 300) { + return Search.snarf(conn.getInputStream()); + } else { + throw new IOException("Fetch failed, status " + status + ": " + Search.snarf(conn.getErrorStream())); + } + + } catch (MalformedURLException e) { + throw new IOException(e); + } + + } + + public static GenericHttpsResource createNew (URI uri) { + HashSet flags = new HashSet(); + flags.add("generic"); + HashMap params = new HashMap(); + return create(flags, params, uri); + } + + public static GenericHttpsResource create(Set flags, HashMap params, URI uri) { + if ( ! ("https".equals(uri.getScheme()) + && flags != null && flags.size() == 1 && flags.contains("generic") + && (params == null || params.isEmpty()))) { + return null; + } + return new GenericHttpsResource(flags, params, uri); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/TwitterResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/TwitterResource.java new file mode 100644 index 000000000..4fc3590f8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/TwitterResource.java @@ -0,0 +1,4 @@ +package org.sufficientlysecure.keychain.pgp.affirmation.resources; + +public class TwitterResource { +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/UnknownResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/UnknownResource.java new file mode 100644 index 000000000..e2d050eb4 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/affirmation/resources/UnknownResource.java @@ -0,0 +1,20 @@ +package org.sufficientlysecure.keychain.pgp.affirmation.resources; + +import org.sufficientlysecure.keychain.pgp.affirmation.AffirmationResource; + +import java.net.URI; +import java.util.HashMap; +import java.util.Set; + +public class UnknownResource extends AffirmationResource { + + public UnknownResource(Set flags, HashMap params, URI uri) { + super(flags, params, uri); + } + + @Override + public boolean verify() { + return false; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java index f1453c40c..3dd4258e7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java @@ -37,6 +37,7 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.ui.affirmations.AffirmationWizard; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; @@ -55,6 +56,7 @@ public class ViewKeyMainFragment extends LoaderFragment implements public static final String ARG_DATA_URI = "uri"; + private View mActionLink, mActionLinkDivider; private View mActionEdit; private View mActionEditDivider; private View mActionEncryptFiles; @@ -83,6 +85,8 @@ public class ViewKeyMainFragment extends LoaderFragment implements View view = inflater.inflate(R.layout.view_key_main_fragment, getContainer()); mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids); + mActionLink = view.findViewById(R.id.view_key_action_link); + mActionLinkDivider = view.findViewById(R.id.view_key_action_link); mActionEdit = view.findViewById(R.id.view_key_action_edit); mActionEditDivider = view.findViewById(R.id.view_key_action_edit_divider); mActionEncryptText = view.findViewById(R.id.view_key_action_encrypt_text); @@ -156,6 +160,11 @@ public class ViewKeyMainFragment extends LoaderFragment implements certify(mDataUri); } }); + mActionLink.setOnClickListener(new View.OnClickListener() { + public void onClick(View view) { + linkKey(mDataUri); + } + }); mActionEdit.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { editKey(mDataUri); @@ -224,10 +233,14 @@ public class ViewKeyMainFragment extends LoaderFragment implements if (data.moveToFirst()) { if (data.getInt(INDEX_UNIFIED_HAS_ANY_SECRET) != 0) { // edit button + mActionLink.setVisibility(View.VISIBLE); + mActionLinkDivider.setVisibility(View.VISIBLE); mActionEdit.setVisibility(View.VISIBLE); mActionEditDivider.setVisibility(View.VISIBLE); } else { // edit button + mActionLink.setVisibility(View.GONE); + mActionLinkDivider.setVisibility(View.GONE); mActionEdit.setVisibility(View.GONE); mActionEditDivider.setVisibility(View.GONE); } @@ -347,4 +360,10 @@ public class ViewKeyMainFragment extends LoaderFragment implements startActivityForResult(editIntent, 0); } + private void linkKey(Uri dataUri) { + Intent editIntent = new Intent(getActivity(), AffirmationWizard.class); + editIntent.setData(KeyRings.buildUnifiedKeyRingUri(dataUri)); + startActivityForResult(editIntent, 0); + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationCreateHttpsStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationCreateHttpsStep1Fragment.java new file mode 100644 index 000000000..30cfe9a79 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationCreateHttpsStep1Fragment.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * 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.affirmations; + +import android.app.ProgressDialog; +import android.content.Intent; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Patterns; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.EditText; + +import org.openintents.openpgp.util.OpenPgpApi; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.SignEncryptResult; +import org.sufficientlysecure.keychain.pgp.affirmation.resources.GenericHttpsResource; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.ui.NfcActivity; +import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity; + +import java.util.Date; + +public class AffirmationCreateHttpsStep1Fragment extends Fragment { + + public static final int REQUEST_CODE_PASSPHRASE = 0x00008001; + public static final int REQUEST_CODE_NFC = 0x00008002; + + AffirmationWizard mAffirmationWizard; + + String mProofUri, mProofText; + + EditText mEditUri; + + // For NFC data + protected String mSigningKeyPassphrase = null; + protected Date mNfcTimestamp = null; + protected byte[] mNfcHash = null; + + /** + * Creates new instance of this fragment + */ + public static AffirmationCreateHttpsStep1Fragment newInstance() { + AffirmationCreateHttpsStep1Fragment frag = new AffirmationCreateHttpsStep1Fragment(); + + Bundle args = new Bundle(); + frag.setArguments(args); + + return frag; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mAffirmationWizard = (AffirmationWizard) getActivity(); + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.affirmation_create_https_fragment_step1, container, false); + + view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + + String uri = "https://" + mEditUri.getText(); + + if (!checkUri(uri)) { + return; + } + + mProofUri = uri; + mProofText = GenericHttpsResource.generate(mAffirmationWizard.mFingerprint, null); + + generateResourceAndNext(); + } + }); + + mEditUri = (EditText) view.findViewById(R.id.affirmation_create_https_uri); + + mEditUri.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { + } + + @Override + public void afterTextChanged(Editable editable) { + String uri = "https://" + editable; + if (uri.length() > 0) { + if (checkUri(uri)) { + mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, + R.drawable.uid_mail_ok, 0); + } else { + mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, + R.drawable.uid_mail_bad, 0); + } + } else { + // remove drawable if email is empty + mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + } + }); + + mEditUri.setText("mugenguild.com/pgpkey.txt"); + + return view; + } + + public void generateResourceAndNext () { + + // Send all information needed to service to edit key in other thread + Intent intent = new Intent(getActivity(), KeychainIntentService.class); + intent.setAction(KeychainIntentService.ACTION_SIGN_ENCRYPT); + intent.putExtra(KeychainIntentService.EXTRA_DATA, createEncryptBundle()); + + // Message is received after encrypting is done in KeychainIntentService + KeychainIntentServiceHandler serviceHandler = new KeychainIntentServiceHandler( + mAffirmationWizard, getString(R.string.progress_encrypting), + ProgressDialog.STYLE_HORIZONTAL) { + public void handleMessage(Message message) { + // handle messages by standard KeychainIntentServiceHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + SignEncryptResult pgpResult = + message.getData().getParcelable(SignEncryptResult.EXTRA_RESULT); + + if (pgpResult.isPending()) { + if ((pgpResult.getResult() & SignEncryptResult.RESULT_PENDING_PASSPHRASE) == + SignEncryptResult.RESULT_PENDING_PASSPHRASE) { + startPassphraseDialog(pgpResult.getKeyIdPassphraseNeeded()); + } else if ((pgpResult.getResult() & SignEncryptResult.RESULT_PENDING_NFC) == + SignEncryptResult.RESULT_PENDING_NFC) { + + mNfcTimestamp = pgpResult.getNfcTimestamp(); + startNfcSign(pgpResult.getNfcKeyId(), pgpResult.getNfcPassphrase(), pgpResult.getNfcHash(), pgpResult.getNfcAlgo()); + } else { + throw new RuntimeException("Unhandled pending result!"); + } + return; + } + + if (pgpResult.success()) { + String proofText = new String( + message.getData().getByteArray(KeychainIntentService.RESULT_BYTES)); + + AffirmationCreateHttpsStep2Fragment frag = + AffirmationCreateHttpsStep2Fragment.newInstance(mProofUri, proofText); + + mAffirmationWizard.loadFragment(null, frag, AffirmationWizard.FRAG_ACTION_TO_RIGHT); + } else { + pgpResult.createNotify(getActivity()).show(); + } + + // no matter the result, reset parameters + mSigningKeyPassphrase = null; + mNfcHash = null; + mNfcTimestamp = null; + } + } + }; + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(serviceHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + // show progress dialog + serviceHandler.showProgressDialog(getActivity()); + + // start service with intent + getActivity().startService(intent); + + } + + protected Bundle createEncryptBundle() { + // fill values for this action + Bundle data = new Bundle(); + + data.putInt(KeychainIntentService.SOURCE, KeychainIntentService.IO_BYTES); + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.IO_BYTES); + data.putByteArray(KeychainIntentService.ENCRYPT_MESSAGE_BYTES, mProofText.getBytes()); + + // Always use armor for messages + data.putBoolean(KeychainIntentService.ENCRYPT_USE_ASCII_ARMOR, true); + + data.putLong(KeychainIntentService.ENCRYPT_SIGNATURE_MASTER_ID, mAffirmationWizard.mMasterKeyId); + data.putString(KeychainIntentService.ENCRYPT_SIGNATURE_KEY_PASSPHRASE, mSigningKeyPassphrase); + data.putSerializable(KeychainIntentService.ENCRYPT_SIGNATURE_NFC_TIMESTAMP, mNfcTimestamp); + data.putByteArray(KeychainIntentService.ENCRYPT_SIGNATURE_NFC_HASH, mNfcHash); + + return data; + } + + protected void startPassphraseDialog(long subkeyId) { + Intent intent = new Intent(getActivity(), PassphraseDialogActivity.class); + intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, subkeyId); + startActivityForResult(intent, REQUEST_CODE_PASSPHRASE); + } + + protected void startNfcSign(long keyId, String pin, byte[] hashToSign, int hashAlgo) { + // build PendingIntent for Yubikey NFC operations + Intent intent = new Intent(getActivity(), NfcActivity.class); + intent.setAction(NfcActivity.ACTION_SIGN_HASH); + + // pass params through to activity that it can be returned again later to repeat pgp operation + intent.putExtra(NfcActivity.EXTRA_DATA, new Intent()); // not used, only relevant to OpenPgpService + intent.putExtra(NfcActivity.EXTRA_KEY_ID, keyId); + intent.putExtra(NfcActivity.EXTRA_PIN, pin); + intent.putExtra(NfcActivity.EXTRA_NFC_HASH_TO_SIGN, hashToSign); + intent.putExtra(NfcActivity.EXTRA_NFC_HASH_ALGO, hashAlgo); + + startActivityForResult(intent, REQUEST_CODE_NFC); + } + + private static boolean checkUri(String uri) { + return Patterns.WEB_URL.matcher(uri).matches(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CODE_PASSPHRASE: { + if (resultCode == AffirmationWizard.RESULT_OK && data != null) { + mSigningKeyPassphrase = data.getStringExtra(PassphraseDialogActivity.MESSAGE_DATA_PASSPHRASE); + generateResourceAndNext(); + return; + } + break; + } + + case REQUEST_CODE_NFC: { + if (resultCode == AffirmationWizard.RESULT_OK && data != null) { + mNfcHash = data.getByteArrayExtra(OpenPgpApi.EXTRA_NFC_SIGNED_HASH); + generateResourceAndNext(); + return; + } + break; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + break; + } + } + } + + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationCreateHttpsStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationCreateHttpsStep2Fragment.java new file mode 100644 index 000000000..ee7ba1a77 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationCreateHttpsStep2Fragment.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * 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.affirmations; + +import android.content.Intent; +import android.graphics.PorterDuff; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageView; + +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.pgp.affirmation.resources.GenericHttpsResource; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.util.FileHelper; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URI; +import java.net.URISyntaxException; + +public class AffirmationCreateHttpsStep2Fragment extends Fragment { + + private static final int REQUEST_CODE_OUTPUT = 0x00007007; + + public static final String URI = "uri", TEXT = "text"; + + AffirmationWizard mAffirmationWizard; + + EditText mEditUri; + ImageView mVerifyImage; + View mVerifyProgress; + + String mResourceUri; + String mProofString; + + /** + * Creates new instance of this fragment + */ + public static AffirmationCreateHttpsStep2Fragment newInstance(String uri, String proofText) { + AffirmationCreateHttpsStep2Fragment frag = new AffirmationCreateHttpsStep2Fragment(); + + Bundle args = new Bundle(); + args.putString(URI, uri); + args.putString(TEXT, proofText); + frag.setArguments(args); + + return frag; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.affirmation_create_https_fragment_step2, container, false); + + mResourceUri = getArguments().getString(URI); + mProofString = getArguments().getString(TEXT); + + view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + + // AffirmationCreateHttpsStep2Fragment frag = + // AffirmationCreateHttpsStep2Fragment.newInstance(); + + // mAffirmationWizard.loadFragment(null, frag, AffirmationWizard.FRAG_ACTION_TO_RIGHT); + } + }); + + mVerifyImage = (ImageView) view.findViewById(R.id.verify_image); + mVerifyProgress = view.findViewById(R.id.verify_progress); + + view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + proofSend(); + } + }); + + view.findViewById(R.id.button_save).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + proofSave(); + } + }); + + view.findViewById(R.id.button_verify).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + proofVerify(); + } + }); + + mEditUri = (EditText) view.findViewById(R.id.affirmation_create_https_uri); + mEditUri.setText(mResourceUri); + + setVerifyProgress(false, null); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mAffirmationWizard = (AffirmationWizard) getActivity(); + } + + public void setVerifyProgress(boolean on, Boolean success) { + mVerifyProgress.setVisibility(on ? View.VISIBLE : View.GONE); + mVerifyImage.setVisibility(on ? View.GONE : View.VISIBLE); + if (success == null) { + mVerifyImage.setImageResource(R.drawable.status_signature_unverified_cutout); + mVerifyImage.setColorFilter(getResources().getColor(R.color.tertiary_text_light), + PorterDuff.Mode.SRC_IN); + } else if (success) { + mVerifyImage.setImageResource(R.drawable.status_signature_verified_cutout); + mVerifyImage.setColorFilter(getResources().getColor(R.color.android_green_dark), + PorterDuff.Mode.SRC_IN); + } else { + mVerifyImage.setImageResource(R.drawable.status_signature_unknown_cutout); + mVerifyImage.setColorFilter(getResources().getColor(R.color.android_red_dark), + PorterDuff.Mode.SRC_IN); + } + } + + public void proofVerify() { + setVerifyProgress(true, null); + + try { + final GenericHttpsResource resource = GenericHttpsResource.createNew(new URI(mResourceUri)); + + new AsyncTask() { + + @Override + protected DecryptVerifyResult doInBackground(Void... params) { + + try { + return resource.verify(getActivity(), new ProviderHelper(getActivity()), null); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + @Override + protected void onPostExecute(DecryptVerifyResult result) { + super.onPostExecute(result); + if (result.success()) { + switch (result.getSignatureResult().getStatus()) { + case OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED: + setVerifyProgress(false, true); + break; + default: + setVerifyProgress(false, false); + // on error, show error message + result.createNotify(getActivity()).show(); + } + } else { + setVerifyProgress(false, false); + // on error, show error message + result.createNotify(getActivity()).show(); + } + } + }.execute(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + + } + + private void proofSend() { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, mProofString); + sendIntent.setType("text/plain"); + startActivity(sendIntent); + } + + private void proofSave () { + String state = Environment.getExternalStorageState(); + if (!Environment.MEDIA_MOUNTED.equals(state)) { + Notify.showNotify(getActivity(), "External storage not available!", Style.ERROR); + return; + } + + String targetName = "pgpkey.txt"; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + File targetFile = new File(Constants.Path.APP_DIR, targetName); + FileHelper.saveFile(this, getString(R.string.title_decrypt_to_file), + getString(R.string.specify_file_to_decrypt_to), targetFile, REQUEST_CODE_OUTPUT); + } else { + FileHelper.saveDocument(this, "text/plain", targetName, REQUEST_CODE_OUTPUT); + } + } + + private void saveFile(Uri uri) { + try { + PrintWriter out = + new PrintWriter(getActivity().getContentResolver().openOutputStream(uri)); + out.print(mProofString); + if (out.checkError()) { + Notify.showNotify(getActivity(), "Error writing file!", Style.ERROR); + } + } catch (FileNotFoundException e) { + Notify.showNotify(getActivity(), "File could not be opened for writing!", Style.ERROR); + e.printStackTrace(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CODE_OUTPUT: + if (data == null) { + return; + } + Uri uri = data.getData(); + saveFile(uri); + break; + } + super.onActivityResult(requestCode, resultCode, data); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationSelectFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationSelectFragment.java new file mode 100644 index 000000000..784e75789 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationSelectFragment.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * 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.affirmations; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.sufficientlysecure.keychain.R; + +public class AffirmationSelectFragment extends Fragment { + + AffirmationWizard mAffirmationWizard; + + /** + * Creates new instance of this fragment + */ + public static AffirmationSelectFragment newInstance() { + AffirmationSelectFragment frag = new AffirmationSelectFragment(); + + Bundle args = new Bundle(); + frag.setArguments(args); + + return frag; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.affirmation_select_fragment, container, false); + + view.findViewById(R.id.affirmation_create_https_button) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + AffirmationCreateHttpsStep1Fragment frag = + AffirmationCreateHttpsStep1Fragment.newInstance(); + + mAffirmationWizard.loadFragment(null, frag, AffirmationWizard.FRAG_ACTION_TO_RIGHT); + } + }); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mAffirmationWizard = (AffirmationWizard) getActivity(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationWizard.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationWizard.java new file mode 100644 index 000000000..99b88405a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/affirmations/AffirmationWizard.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * 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.affirmations; + +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.ActionBarActivity; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.Log; + +public class AffirmationWizard extends ActionBarActivity { + + public static final int FRAG_ACTION_START = 0; + public static final int FRAG_ACTION_TO_RIGHT = 1; + public static final int FRAG_ACTION_TO_LEFT = 2; + + long mMasterKeyId; + byte[] mFingerprint; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.create_key_activity); + + try { + Uri uri = getIntent().getData(); + CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(uri); + mMasterKeyId = ring.extractOrGetMasterKeyId(); + mFingerprint = ring.getFingerprint(); + } catch (PgpKeyNotFoundException e) { + Log.e(Constants.TAG, "Invalid uri given, key does not exist!"); + finish(); + return; + } + + // pass extras into fragment + AffirmationSelectFragment frag = AffirmationSelectFragment.newInstance(); + loadFragment(null, frag, FRAG_ACTION_START); + } + + public void loadFragment(Bundle savedInstanceState, Fragment fragment, int action) { + // However, if we're being restored from a previous state, + // then we don't need to do anything and should return or else + // we could end up with overlapping fragments. + if (savedInstanceState != null) { + return; + } + + // Add the fragment to the 'fragment_container' FrameLayout + // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + + switch (action) { + case FRAG_ACTION_START: + transaction.setCustomAnimations(0, 0); + transaction.replace(R.id.create_key_fragment_container, fragment) + .commitAllowingStateLoss(); + break; + case FRAG_ACTION_TO_LEFT: + getSupportFragmentManager().popBackStackImmediate(); + break; + case FRAG_ACTION_TO_RIGHT: + transaction.setCustomAnimations(R.anim.frag_slide_in_from_right, R.anim.frag_slide_out_to_left, + R.anim.frag_slide_in_from_left, R.anim.frag_slide_out_to_right); + transaction.addToBackStack(null); + transaction.replace(R.id.create_key_fragment_container, fragment) + .commitAllowingStateLoss(); + break; + + } + // do it immediately! + getSupportFragmentManager().executePendingTransactions(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/HttpsPrefixedText.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/HttpsPrefixedText.java new file mode 100644 index 000000000..292343eb7 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/HttpsPrefixedText.java @@ -0,0 +1,38 @@ +package org.sufficientlysecure.keychain.ui.widget; + +import android.content.Context; +import android.graphics.*; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.widget.EditText; + +/** */ +public class HttpsPrefixedText extends EditText { + + private String mPrefix; // can be hardcoded for demo purposes + private Rect mPrefixRect = new Rect(); + + public HttpsPrefixedText(Context context, AttributeSet attrs) { + super(context, attrs); + mPrefix = "https://"; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + getPaint().getTextBounds(mPrefix, 0, mPrefix.length(), mPrefixRect); + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + super.onDraw(canvas); + canvas.drawText(mPrefix, super.getCompoundPaddingLeft(), getBaseline(), getPaint()); + } + + @Override + public int getCompoundPaddingLeft() { + return super.getCompoundPaddingLeft() + mPrefixRect.width(); + } + +} \ No newline at end of file diff --git a/OpenKeychain/src/main/res/drawable/ssl_lock.png b/OpenKeychain/src/main/res/drawable/ssl_lock.png new file mode 100644 index 000000000..00c4d8e4f Binary files /dev/null and b/OpenKeychain/src/main/res/drawable/ssl_lock.png differ diff --git a/OpenKeychain/src/main/res/drawable/twitter.png b/OpenKeychain/src/main/res/drawable/twitter.png new file mode 100644 index 000000000..3533e0488 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable/twitter.png differ diff --git a/OpenKeychain/src/main/res/layout/affirmation_create_https_fragment_step1.xml b/OpenKeychain/src/main/res/layout/affirmation_create_https_fragment_step1.xml new file mode 100644 index 000000000..f50a4d9be --- /dev/null +++ b/OpenKeychain/src/main/res/layout/affirmation_create_https_fragment_step1.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/affirmation_create_https_fragment_step2.xml b/OpenKeychain/src/main/res/layout/affirmation_create_https_fragment_step2.xml new file mode 100644 index 000000000..50a0d6514 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/affirmation_create_https_fragment_step2.xml @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + +