From c05441667e151dceb6f5874b290d70a53258061b Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Fri, 7 Nov 2014 12:28:27 -0800 Subject: [PATCH 01/15] Moved from WebView to Spannables, some proof cleanup too --- .../operations/results/OperationResult.java | 9 + .../keychain/pgp/PgpDecryptVerify.java | 162 ++++++- .../service/KeychainIntentService.java | 135 +++++- .../keychain/ui/ViewKeyActivity.java | 6 + .../keychain/ui/ViewKeyTrustFragment.java | 443 ++++++++++++++++++ .../res/layout/view_key_trust_fragment.xml | 105 +++++ OpenKeychain/src/main/res/values/strings.xml | 48 ++ 7 files changed, 886 insertions(+), 22 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java create mode 100644 OpenKeychain/src/main/res/layout/view_key_trust_fragment.xml diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java index dc45fabc3..70d999242 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java @@ -545,6 +545,15 @@ public abstract class OperationResult implements Parcelable { MSG_DC_TRAIL_UNKNOWN (LogLevel.DEBUG, R.string.msg_dc_trail_unknown), MSG_DC_UNLOCKING (LogLevel.INFO, R.string.msg_dc_unlocking), + // verify signed literal data + MSG_VL (LogLevel.INFO, R.string.msg_vl), + MSG_VL_ERROR_MISSING_SIGLIST (LogLevel.ERROR, R.string.msg_vl_error_no_siglist), + MSG_VL_ERROR_MISSING_LITERAL (LogLevel.ERROR, R.string.msg_vl_error_missing_literal), + MSG_VL_ERROR_MISSING_KEY (LogLevel.ERROR, R.string.msg_vl_error_wrong_key), + MSG_VL_CLEAR_SIGNATURE_CHECK (LogLevel.DEBUG, R.string.msg_vl_clear_signature_check), + MSG_VL_ERROR_INTEGRITY_CHECK (LogLevel.ERROR, R.string.msg_vl_error_integrity_check), + MSG_VL_OK (LogLevel.OK, R.string.msg_vl_ok), + // signencrypt MSG_SE_ASYMMETRIC (LogLevel.INFO, R.string.msg_se_asymmetric), MSG_SE_CLEARSIGN_ONLY (LogLevel.DEBUG, R.string.msg_se_clearsign_only), diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java index 4f086c2a6..4161f2928 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java @@ -48,6 +48,7 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.BaseOperation; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; @@ -83,6 +84,8 @@ public class PgpDecryptVerify extends BaseOperation { private Set mAllowedKeyIds; private boolean mDecryptMetadataOnly; private byte[] mDecryptedSessionKey; + private String mRequiredSignerFingerprint; + private boolean mSignedLiteralData; private PgpDecryptVerify(Builder builder) { super(builder.mContext, builder.mProviderHelper, builder.mProgressable); @@ -96,6 +99,8 @@ public class PgpDecryptVerify extends BaseOperation { this.mAllowedKeyIds = builder.mAllowedKeyIds; this.mDecryptMetadataOnly = builder.mDecryptMetadataOnly; this.mDecryptedSessionKey = builder.mDecryptedSessionKey; + this.mSignedLiteralData = builder.mSignedLiteralData; + this.mRequiredSignerFingerprint = builder.mRequiredSignerFingerprint; } public static class Builder { @@ -112,6 +117,8 @@ public class PgpDecryptVerify extends BaseOperation { private Set mAllowedKeyIds = null; private boolean mDecryptMetadataOnly = false; private byte[] mDecryptedSessionKey = null; + private String mRequiredSignerFingerprint = null; + private boolean mSignedLiteralData = false; public Builder(Context context, ProviderHelper providerHelper, Progressable progressable, @@ -123,6 +130,24 @@ public class PgpDecryptVerify extends BaseOperation { mOutStream = outStream; } + /** + * This is used when verifying signed literals to check that they are signed with + * the required key + */ + public Builder setRequiredSignerFingerprint(String fingerprint) { + mRequiredSignerFingerprint = fingerprint; + return this; + } + + /** + * This is to force a mode where the message is just the signature key id and + * then a literal data packet; used in Keybase.io proofs + */ + public Builder setSignedLiteralData(boolean signedLiteralData) { + mSignedLiteralData = signedLiteralData; + return this; + } + public Builder setAllowSymmetricDecryption(boolean allowSymmetricDecryption) { mAllowSymmetricDecryption = allowSymmetricDecryption; return this; @@ -174,7 +199,9 @@ public class PgpDecryptVerify extends BaseOperation { // it is ascii armored Log.d(Constants.TAG, "ASCII Armor Header Line: " + aIn.getArmorHeaderLine()); - if (aIn.isClearText()) { + if (mSignedLiteralData) { + return verifySignedLiteralData(aIn, 0); + } else if (aIn.isClearText()) { // a cleartext signature, verify it with the other method return verifyCleartextSignature(aIn, 0); } @@ -195,6 +222,139 @@ public class PgpDecryptVerify extends BaseOperation { } } + /** + * Verify Keybase.io style signed literal data + */ + private DecryptVerifyResult verifySignedLiteralData(InputStream in, int indent) throws IOException, PGPException { + OperationLog log = new OperationLog(); + log.add(LogType.MSG_VL, indent); + + // thinking that the proof-fetching operation is going to take most of the time + updateProgress(R.string.progress_reading_data, 75, 100); + + PGPObjectFactory pgpF = new PGPObjectFactory(in, new JcaKeyFingerprintCalculator()); + Object o = pgpF.nextObject(); + if (o instanceof PGPCompressedData) { + log.add(LogType.MSG_DC_CLEAR_DECOMPRESS, indent + 1); + + pgpF = new PGPObjectFactory(((PGPCompressedData) o).getDataStream(), new JcaKeyFingerprintCalculator()); + o = pgpF.nextObject(); + updateProgress(R.string.progress_decompressing_data, 80, 100); + } + + // all we want to see is a OnePassSignatureList followed by LiteralData + if (!(o instanceof PGPOnePassSignatureList)) { + log.add(LogType.MSG_VL_ERROR_MISSING_SIGLIST, indent); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + } + PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) o; + + // go through all signatures (should be just one), make sure we have + // the key and it matches the one we’re looking for + CanonicalizedPublicKeyRing signingRing = null; + CanonicalizedPublicKey signingKey = null; + int signatureIndex = -1; + for (int i = 0; i < sigList.size(); ++i) { + try { + long sigKeyId = sigList.get(i).getKeyID(); + signingRing = mProviderHelper.getCanonicalizedPublicKeyRing( + KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId) + ); + signingKey = signingRing.getPublicKey(sigKeyId); + signatureIndex = i; + } catch (ProviderHelper.NotFoundException e) { + Log.d(Constants.TAG, "key not found, trying next signature..."); + } + } + + // there has to be a key, and it has to be the right one + if (signingKey == null) { + log.add(LogType.MSG_VL_ERROR_MISSING_KEY, indent); + Log.d(Constants.TAG, "Failed to find key in signed-literal message"); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + } + + CanonicalizedPublicKey encryptKey = signingKey; + try { + encryptKey = signingRing.getEncryptionSubKey(); + } catch (PgpKeyNotFoundException e) { + } + String fingerprint = KeyFormattingUtils.convertFingerprintToHex(signingKey.getFingerprint()); + String cryptFingerprint = KeyFormattingUtils.convertFingerprintToHex(encryptKey.getFingerprint()); + if (!(mRequiredSignerFingerprint.equals(fingerprint) || mRequiredSignerFingerprint.equals(cryptFingerprint))) { + log.add(LogType.MSG_VL_ERROR_MISSING_KEY, indent); + Log.d(Constants.TAG, "Key mismatch; wanted " + mRequiredSignerFingerprint + + " got " + fingerprint + "/" + cryptFingerprint); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + } + + OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder(); + + PGPOnePassSignature signature = sigList.get(signatureIndex); + signatureResultBuilder.initValid(signingRing, signingKey); + + JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = + new JcaPGPContentVerifierBuilderProvider() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + signature.init(contentVerifierBuilderProvider, signingKey.getPublicKey()); + + o = pgpF.nextObject(); + + if (!(o instanceof PGPLiteralData)) { + log.add(LogType.MSG_VL_ERROR_MISSING_LITERAL, indent); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + } + + PGPLiteralData literalData = (PGPLiteralData) o; + + log.add(LogType.MSG_DC_CLEAR_DATA, indent + 1); + updateProgress(R.string.progress_decrypting, 85, 100); + + InputStream dataIn = literalData.getInputStream(); + + int length; + byte[] buffer = new byte[1 << 16]; + while ((length = dataIn.read(buffer)) > 0) { + mOutStream.write(buffer, 0, length); + signature.update(buffer, 0, length); + } + + updateProgress(R.string.progress_verifying_signature, 95, 100); + log.add(LogType.MSG_VL_CLEAR_SIGNATURE_CHECK, indent + 1); + + PGPSignatureList signatureList = (PGPSignatureList) pgpF.nextObject(); + PGPSignature messageSignature = signatureList.get(signatureIndex); + + // these are not cleartext signatures! + // TODO: what about binary signatures? + signatureResultBuilder.setSignatureOnly(false); + + // Verify signature and check binding signatures + boolean validSignature = signature.verify(messageSignature); + if (validSignature) { + log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1); + } else { + log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1); + } + signatureResultBuilder.setValidSignature(validSignature); + + if (!signatureResultBuilder.isValidSignature()) { + log.add(LogType.MSG_VL_ERROR_INTEGRITY_CHECK, indent); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + } + + updateProgress(R.string.progress_done, 100, 100); + + log.add(LogType.MSG_VL_OK, indent); + + // Return a positive result, with metadata and verification info + DecryptVerifyResult result = + new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); + result.setSignatureResult(signatureResultBuilder.build()); + return result; + } + + /** * Decrypt and/or verifies binary or ascii armored pgp */ diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java index a2988f2b2..42e0c7cc9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -26,55 +26,62 @@ import android.os.Message; import android.os.Messenger; import android.os.RemoteException; +import com.textuality.keybase.lib.Proof; +import com.textuality.keybase.lib.prover.Prover; + +import org.json.JSONObject; +import org.spongycastle.openpgp.PGPUtil; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.operations.CertifyOperation; -import org.sufficientlysecure.keychain.operations.DeleteOperation; -import org.sufficientlysecure.keychain.operations.results.DeleteResult; -import org.sufficientlysecure.keychain.operations.results.ExportResult; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.operations.results.CertifyResult; -import org.sufficientlysecure.keychain.util.FileHelper; -import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; -import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; import org.sufficientlysecure.keychain.keyimport.Keyserver; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.operations.CertifyOperation; +import org.sufficientlysecure.keychain.operations.DeleteOperation; +import org.sufficientlysecure.keychain.operations.ImportExportOperation; +import org.sufficientlysecure.keychain.operations.results.CertifyResult; +import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.operations.results.DeleteResult; +import org.sufficientlysecure.keychain.operations.results.EditKeyResult; +import org.sufficientlysecure.keychain.operations.results.ExportResult; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; +import org.sufficientlysecure.keychain.operations.results.SignEncryptResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; -import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.pgp.PgpHelper; -import org.sufficientlysecure.keychain.operations.ImportExportOperation; import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; import org.sufficientlysecure.keychain.pgp.PgpSignEncrypt; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralMsgIdException; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; -import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; -import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; -import org.sufficientlysecure.keychain.operations.results.EditKeyResult; -import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; -import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; -import org.sufficientlysecure.keychain.operations.results.SignEncryptResult; -import org.sufficientlysecure.keychain.util.ParcelableFileCache; +import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.util.InputData; import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ParcelableFileCache; +import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; +import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.ProgressScaler; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -93,6 +100,8 @@ public class KeychainIntentService extends IntentService implements Progressable public static final String ACTION_DECRYPT_VERIFY = Constants.INTENT_PREFIX + "DECRYPT_VERIFY"; + public static final String ACTION_VERIFY_KEYBASE_PROOF = Constants.INTENT_PREFIX + "VERIFY_KEYBASE_PROOF"; + public static final String ACTION_DECRYPT_METADATA = Constants.INTENT_PREFIX + "DECRYPT_METADATA"; public static final String ACTION_EDIT_KEYRING = Constants.INTENT_PREFIX + "EDIT_KEYRING"; @@ -142,6 +151,10 @@ public class KeychainIntentService extends IntentService implements Progressable public static final String DECRYPT_PASSPHRASE = "passphrase"; public static final String DECRYPT_NFC_DECRYPTED_SESSION_KEY = "nfc_decrypted_session_key"; + // keybase proof + public static final String KEYBASE_REQUIRED_FINGERPRINT = "keybase_required_fingerprint"; + public static final String KEYBASE_PROOF = "keybase_proof"; + // save keyring public static final String EDIT_KEYRING_PARCEL = "save_parcel"; public static final String EDIT_KEYRING_PASSPHRASE = "passphrase"; @@ -291,6 +304,72 @@ public class KeychainIntentService extends IntentService implements Progressable sendErrorToHandler(e); } + } else if (ACTION_VERIFY_KEYBASE_PROOF.equals(action)) { + try { + Proof proof = new Proof(new JSONObject(data.getString(KEYBASE_PROOF))); + setProgress(R.string.keybase_message_fetching_data, 0, 100); + + Prover prover = Prover.findProverFor(proof); + + if (prover == null) { + sendProofError(getString(R.string.keybase_no_prover_found) + ": " + proof.getPrettyName()); + return; + } + + if (!prover.fetchProofData()) { + sendProofError(prover.getLog(), getString(R.string.keybase_problem_fetching_evidence)); + return; + } + + byte[] messageBytes = prover.getPgpMessage().getBytes(); + if (prover.rawMessageCheckRequired()) { + InputStream messageByteStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(messageBytes)); + String problem = prover.checkRawMessageBytes(messageByteStream); + if (problem != null) { + sendProofError(prover.getLog(), problem); + return; + } + } + + // kind of awkward, but this whole class wants to pull bytes out of “data” + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.IO_BYTES); + data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, messageBytes); + + InputData inputData = createDecryptInputData(data); + OutputStream outStream = createCryptOutputStream(data); + String fingerprint = data.getString(KEYBASE_REQUIRED_FINGERPRINT); + + PgpDecryptVerify.Builder builder = new PgpDecryptVerify.Builder( + this, new ProviderHelper(this), this, + inputData, outStream + ); + builder.setSignedLiteralData(true).setRequiredSignerFingerprint(fingerprint); + + DecryptVerifyResult decryptVerifyResult = builder.build().execute(); + outStream.close(); + + if (!decryptVerifyResult.success()) { + OperationLog log = decryptVerifyResult.getLog(); + OperationResult.LogEntryParcel lastEntry = null; + for (OperationResult.LogEntryParcel entry : log) { + lastEntry = entry; + } + sendProofError(getString(lastEntry.mType.getMsgId())); + return; + } + + if (!prover.validate(outStream.toString())) { + sendProofError(getString(R.string.keybase_message_payload_mismatch)); + return; + } + + Bundle resultData = new Bundle(); + resultData.putString(KeychainIntentServiceHandler.DATA_MESSAGE, "OK"); + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); + } catch (Exception e) { + sendErrorToHandler(e); + } + } else if (ACTION_DECRYPT_VERIFY.equals(action)) { try { @@ -597,6 +676,21 @@ public class KeychainIntentService extends IntentService implements Progressable } + private void sendProofError(List log, String label) { + String msg = null; + for (String m : log) { + Log.e(Constants.TAG, label + ": " + m); + msg = m; + } + sendProofError(label + ": " + msg); + } + + private void sendProofError(String msg) { + Bundle bundle = new Bundle(); + bundle.putString(KeychainIntentServiceHandler.DATA_ERROR, msg); + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, bundle); + } + private void sendErrorToHandler(Exception e) { // TODO: Implement a better exception handling here // contextualize the exception, if necessary @@ -607,7 +701,6 @@ public class KeychainIntentService extends IntentService implements Progressable } else { message = e.getMessage(); } - Log.d(Constants.TAG, "KeychainIntentService Exception: ", e); Bundle data = new Bundle(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index 0bc75b3a9..a7ba4accf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -78,6 +78,7 @@ public class ViewKeyActivity extends ActionBarActivity implements public static final String EXTRA_SELECTED_TAB = "selected_tab"; public static final int TAB_MAIN = 0; public static final int TAB_SHARE = 1; + public static final int TAB_TRUST = 2; // view private ViewPager mViewPager; @@ -183,6 +184,11 @@ public class ViewKeyActivity extends ActionBarActivity implements mTabsAdapter.addTab(ViewKeyShareFragment.class, shareBundle, getString(R.string.key_view_tab_share)); + Bundle trustBundle = new Bundle(); + trustBundle.putParcelable(ViewKeyMainFragment.ARG_DATA_URI, dataUri); + mTabsAdapter.addTab(ViewKeyTrustFragment.class, trustBundle, + getString(R.string.key_view_tab_trust)); + // update layout after operations mSlidingTabLayout.setViewPager(mViewPager); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java new file mode 100644 index 000000000..ef6cd50f1 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2014 Tim Bray + * + * 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.ProgressDialog; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.text.style.StyleSpan; +import android.text.style.URLSpan; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TableLayout; +import android.widget.TableRow; +import android.widget.TextView; + +import com.textuality.keybase.lib.KeybaseException; +import com.textuality.keybase.lib.Proof; +import com.textuality.keybase.lib.User; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.ArrayList; +import java.util.Date; +import java.util.Hashtable; +import java.util.List; + +public class ViewKeyTrustFragment extends LoaderFragment implements + LoaderManager.LoaderCallbacks { + + private View mStartSearch; + private TextView mTrustReadout; + private TextView mReportHeader; + private TableLayout mProofListing; + private LayoutInflater mInflater; + private View mProofVerifyHeader; + private TextView mProofVerifyDetail; + + private static final int LOADER_ID_DATABASE = 1; + + // for retrieving the key we’re working on + private Uri mDataUri; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { + View root = super.onCreateView(inflater, superContainer, savedInstanceState); + View view = inflater.inflate(R.layout.view_key_trust_fragment, getContainer()); + mInflater = inflater; + + mTrustReadout = (TextView) view.findViewById(R.id.view_key_trust_readout); + mStartSearch = view.findViewById(R.id.view_key_trust_search_cloud); + mStartSearch.setEnabled(false); + mReportHeader = (TextView) view.findViewById(R.id.view_key_trust_cloud_narrative); + mProofListing = (TableLayout) view.findViewById(R.id.view_key_proof_list); + mProofVerifyHeader = view.findViewById(R.id.view_key_proof_verify_header); + mProofVerifyDetail = (TextView) view.findViewById(R.id.view_key_proof_verify_detail); + mReportHeader.setVisibility(View.GONE); + mProofListing.setVisibility(View.GONE); + mProofVerifyHeader.setVisibility(View.GONE); + mProofVerifyDetail.setVisibility(View.GONE); + + return root; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + Uri dataUri = getArguments().getParcelable(ViewKeyMainFragment.ARG_DATA_URI); + if (dataUri == null) { + Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); + getActivity().finish(); + return; + } + mDataUri = dataUri; + + // retrieve the key from the database + getLoaderManager().initLoader(LOADER_ID_DATABASE, null, this); + } + + static final String[] TRUST_PROJECTION = new String[]{ + KeyRings._ID, KeyRings.FINGERPRINT, KeyRings.IS_REVOKED, KeyRings.EXPIRY, + KeyRings.HAS_ANY_SECRET, KeyRings.VERIFIED + }; + static final int INDEX_TRUST_FINGERPRINT = 1; + static final int INDEX_TRUST_IS_REVOKED = 2; + static final int INDEX_TRUST_EXPIRY = 3; + static final int INDEX_UNIFIED_HAS_ANY_SECRET = 4; + static final int INDEX_VERIFIED = 5; + + public Loader onCreateLoader(int id, Bundle args) { + setContentShown(false); + + switch (id) { + case LOADER_ID_DATABASE: { + Uri baseUri = KeyRings.buildUnifiedKeyRingUri(mDataUri); + return new CursorLoader(getActivity(), baseUri, TRUST_PROJECTION, null, null, null); + } + // decided to just use an AsyncTask for keybase, but maybe later + default: + return null; + } + } + + public void onLoadFinished(Loader loader, Cursor data) { + /* TODO better error handling? May cause problems when a key is deleted, + * because the notification triggers faster than the activity closes. + */ + // Avoid NullPointerExceptions... + if (data.getCount() == 0) { + return; + } + + boolean nothingSpecial = true; + StringBuilder message = new StringBuilder(); + + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + if (data.moveToFirst()) { + + if (data.getInt(INDEX_UNIFIED_HAS_ANY_SECRET) != 0) { + message.append(getString(R.string.key_trust_it_is_yours)).append("\n"); + nothingSpecial = false; + } else if (data.getInt(INDEX_VERIFIED) != 0) { + message.append(getString(R.string.key_trust_already_verified)).append("\n"); + nothingSpecial = false; + } + + // If this key is revoked, don’t trust it! + if (data.getInt(INDEX_TRUST_IS_REVOKED) != 0) { + message.append(getString(R.string.key_trust_revoked)). + append(getString(R.string.key_trust_old_keys)); + + nothingSpecial = false; + } else { + Date expiryDate = new Date(data.getLong(INDEX_TRUST_EXPIRY) * 1000); + if (!data.isNull(INDEX_TRUST_EXPIRY) && expiryDate.before(new Date())) { + + // if expired, don’t trust it! + message.append(getString(R.string.key_trust_expired)). + append(getString(R.string.key_trust_old_keys)); + + nothingSpecial = false; + } + } + + if (nothingSpecial) { + message.append(getString(R.string.key_trust_maybe)); + } + + final byte[] fp = data.getBlob(INDEX_TRUST_FINGERPRINT); + final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fp); + if (fingerprint != null) { + + mStartSearch.setEnabled(true); + mStartSearch.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + new DescribeKey().execute(fingerprint); + } + }); + } + } + + mTrustReadout.setText(message); + setContentShown(true); + } + + /** + * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. + * We need to make sure we are no longer using it. + */ + public void onLoaderReset(Loader loader) { + // no-op in this case I think + } + + class ResultPage { + String mHeader; + final List mProofs; + + public ResultPage(String header, List proofs) { + mHeader = header; + mProofs = proofs; + } + } + + // look for evidence from keybase in the background, make tabular version of result + // + private class DescribeKey extends AsyncTask { + + @Override + protected ResultPage doInBackground(String... args) { + String fingerprint = args[0]; + + final ArrayList proofList = new ArrayList(); + final Hashtable> proofs = new Hashtable>(); + try { + User keybaseUser = User.findByFingerprint(fingerprint); + for (Proof proof : keybaseUser.getProofs()) { + Integer proofType = proof.getType(); + appendIfOK(proofs, proofType, proof); + } + + // a one-liner in a modern programming language + for (Integer proofType : proofs.keySet()) { + Proof[] x = {}; + Proof[] proofsFor = proofs.get(proofType).toArray(x); + if (proofsFor.length > 0) { + SpannableStringBuilder ssb = new SpannableStringBuilder(); + ssb.append(getProofNarrative(proofType)).append(" "); + + int i = 0; + while (i < proofsFor.length - 1) { + appendProofLinks(ssb, fingerprint, proofsFor[i]); + ssb.append(", "); + i++; + } + appendProofLinks(ssb, fingerprint, proofsFor[i]); + proofList.add(ssb); + } + } + + } catch (KeybaseException ignored) { + } + + return new ResultPage(getString(R.string.key_trust_results_prefix), proofList); + } + + private SpannableStringBuilder appendProofLinks(SpannableStringBuilder ssb, final String fingerprint, final Proof proof) throws KeybaseException { + int startAt = ssb.length(); + String handle = proof.getHandle(); + ssb.append(handle); + ssb.setSpan(new URLSpan(proof.getServiceUrl()), startAt, startAt + handle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + if (haveProofFor(proof.getType())) { + ssb.append("\u00a0["); + startAt = ssb.length(); + ssb.append("Verify"); + ClickableSpan clicker = new ClickableSpan() { + @Override + public void onClick(View view) { + verify(proof, fingerprint); + } + }; + ssb.setSpan(clicker, startAt, startAt + "Verify".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.append("]"); + } + return ssb; + } + + @Override + protected void onPostExecute(ResultPage result) { + super.onPostExecute(result); + if (result.mHeader == null) { + result.mHeader = getActivity().getString(R.string.key_trust_no_cloud_evidence); + } + + mStartSearch.setVisibility(View.GONE); + mReportHeader.setVisibility(View.VISIBLE); + mProofListing.setVisibility(View.VISIBLE); + mReportHeader.setText(result.mHeader); + + int rowNumber = 1; + for (CharSequence s : result.mProofs) { + TableRow row = (TableRow) mInflater.inflate(R.layout.view_key_keybase_proof, null); + TextView number = (TextView) row.findViewById(R.id.proof_number); + TextView text = (TextView) row.findViewById(R.id.proof_text); + number.setText(Integer.toString(rowNumber++) + ". "); + text.setText(s); + text.setMovementMethod(LinkMovementMethod.getInstance()); + mProofListing.addView(row); + } + + // mSearchReport.loadDataWithBaseURL("file:///android_res/drawable/", s, "text/html", "UTF-8", null); + } + } + + private String getProofNarrative(int proofType) { + int stringIndex; + switch (proofType) { + case Proof.PROOF_TYPE_TWITTER: stringIndex = R.string.keybase_narrative_twitter; break; + case Proof.PROOF_TYPE_GITHUB: stringIndex = R.string.keybase_narrative_github; break; + case Proof.PROOF_TYPE_DNS: stringIndex = R.string.keybase_narrative_dns; break; + case Proof.PROOF_TYPE_WEB_SITE: stringIndex = R.string.keybase_narrative_web_site; break; + case Proof.PROOF_TYPE_HACKERNEWS: stringIndex = R.string.keybase_narrative_hackernews; break; + case Proof.PROOF_TYPE_COINBASE: stringIndex = R.string.keybase_narrative_coinbase; break; + case Proof.PROOF_TYPE_REDDIT: stringIndex = R.string.keybase_narrative_reddit; break; + default: stringIndex = R.string.keybase_narrative_unknown; + } + return getActivity().getString(stringIndex); + } + + private void appendIfOK(Hashtable> table, Integer proofType, Proof proof) throws KeybaseException { + if (!proofIsOK(proof)) { + return; + } + ArrayList list = table.get(proofType); + if (list == null) { + list = new ArrayList(); + table.put(proofType, list); + } + list.add(proof); + } + + // We only accept http & https proofs. Maybe whitelist later? + private boolean proofIsOK(Proof proof) throws KeybaseException { + Uri uri = Uri.parse(proof.getServiceUrl()); + String scheme = uri.getScheme(); + return ("https".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme)); + } + + // which proofs do we have working verifiers for? + private boolean haveProofFor(int proofType) { + switch (proofType) { + case Proof.PROOF_TYPE_TWITTER: return true; + case Proof.PROOF_TYPE_GITHUB: return true; + case Proof.PROOF_TYPE_DNS: return false; + case Proof.PROOF_TYPE_WEB_SITE: return true; + case Proof.PROOF_TYPE_HACKERNEWS: return true; + case Proof.PROOF_TYPE_COINBASE: return false; + case Proof.PROOF_TYPE_REDDIT: return false; + default: return false; + } + } + + private void verify(final Proof proof, final String fingerprint) { + Intent intent = new Intent(getActivity(), KeychainIntentService.class); + Bundle data = new Bundle(); + intent.setAction(KeychainIntentService.ACTION_VERIFY_KEYBASE_PROOF); + + data.putString(KeychainIntentService.KEYBASE_PROOF, proof.toString()); + data.putString(KeychainIntentService.KEYBASE_REQUIRED_FINGERPRINT, fingerprint); + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + mProofVerifyDetail.setVisibility(View.GONE); + + // Create a new Messenger for the communication back after proof work is done + // + KeychainIntentServiceHandler handler = new KeychainIntentServiceHandler(getActivity(), + getString(R.string.progress_decrypting), ProgressDialog.STYLE_HORIZONTAL) { + public void handleMessage(Message message) { + // handle messages by standard KeychainIntentServiceHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + Bundle returnData = message.getData(); + String msg = returnData.getString(KeychainIntentServiceHandler.DATA_MESSAGE); + SpannableStringBuilder ssb = new SpannableStringBuilder(); + + if ((msg != null) && msg.equals("OK")) { + //yay + String serviceUrl, urlLabel, postUrl; + try { + serviceUrl = proof.getServiceUrl(); + if (serviceUrl.startsWith("https://")) { + urlLabel = serviceUrl.substring("https://".length()); + } else if (serviceUrl.startsWith("http://")) { + urlLabel = serviceUrl.substring("http://".length()); + } else { + urlLabel = serviceUrl; + } + postUrl = proof.getHumanUrl(); + + } catch (KeybaseException e) { + throw new RuntimeException(e); + } + ssb.append(getString(R.string.keybase_proof_succeeded)); + StyleSpan bold = new StyleSpan(Typeface.BOLD); + ssb.setSpan(bold, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.append("\n\n"); + int length = ssb.length(); + String segment = getString(R.string.keybase_a_post); + ssb.append(segment); + URLSpan postLink = new URLSpan(postUrl); + ssb.setSpan(postLink, length, length + segment.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.append(" ").append(getString(R.string.keybase_fetched_from)).append(" "); + URLSpan serviceLink = new URLSpan(serviceUrl); + length = ssb.length(); + ssb.append(urlLabel); + ssb.setSpan(serviceLink, length, length + urlLabel.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.append(" ").append(getString(R.string.keybase_contained_signature)); + + } else { + msg = returnData.getString(KeychainIntentServiceHandler.DATA_ERROR); + ssb.append(getString(R.string.keybase_proof_failure)); + if (msg == null) { + msg = getString(R.string.keybase_unknown_proof_failure); + StyleSpan bold = new StyleSpan(Typeface.BOLD); + ssb.setSpan(bold, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.append("\n\n").append(msg); + } + } + mProofVerifyHeader.setVisibility(View.VISIBLE); + mProofVerifyDetail.setVisibility(View.VISIBLE); + mProofVerifyDetail.setText(ssb); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(handler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + // show progress dialog + handler.showProgressDialog(getActivity()); + + // start service with intent + getActivity().startService(intent); + } +} diff --git a/OpenKeychain/src/main/res/layout/view_key_trust_fragment.xml b/OpenKeychain/src/main/res/layout/view_key_trust_fragment.xml new file mode 100644 index 000000000..f97401271 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/view_key_trust_fragment.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index e2b92d875..f582bd746 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -44,6 +44,9 @@ "Identities" + "Should you trust this key?" + Proof verification + "Evidence from the cloud" "Subkeys" "Cloud search" "General" @@ -525,6 +528,7 @@ "Share" "Subkeys" "Certificates" + "Trust this key?" "Revoked" "This identity has been revoked by the key owner. It is no longer valid." "Certified" @@ -534,6 +538,37 @@ "Invalid" "Something is wrong with this identity!" + + "You have already certified this key!" + "This is one of your keys!" + "This key is neither revoked nor expired.\nYou haven’t certified it, but you may choose to trust it." + "This key has been revoked by its owner. You should not trust it." + "This key has expired. You should not trust it." + " It may be OK to use this to decrypt an old message dating from the time when this key was valid." + "No evidence from the cloud on this key’s trustworthiness." + "Start search" + "Keybase.io offers “proofs” which assert that the owner of this key: " + + + "Posts to Twitter as" + "Is known on GitHub as" + "Controls the domain name(s)" + "Can post to the Web site(s)" + "Posts to Reddit as" + "Is known on Coinbase as" + "Posts to Hacker News as" + "Unknown proof type" + "Unfortunately this proof cannot be verified." + "Unrecognized problem with proof checker" + "Problem with proof evidence" + "No proof checker found for" + "Decrypted proof post does not match expected value" + "Fetching proof evidence" + "This proof has been verified!" + "A post" + "fetched from" + "contains a message which could only have been created by the owner of this key." + "Change Passphrase" "Add Identity" @@ -894,6 +929,19 @@ "Encountered trailing data of unknown type" "Unlocking secret key" + + "Starting signature check" + "No signature list in signed literal data" + "Message not signed with right key" + "No payload in signed literal data" + "Filename: %s" + "MIME type: %s" + "Modification time: %s" + "Filesize: %s" + "Verifying signature data" + "Integrity check error!" + "OK" + "Preparing public keys for encryption" "Signing of cleartext input not supported!" From 4929e346a2ec2a2e046162d4e7b0870b54c17747 Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Fri, 7 Nov 2014 12:29:17 -0800 Subject: [PATCH 02/15] XML fragment for proof listing --- .../res/layout/view_key_keybase_proof.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 OpenKeychain/src/main/res/layout/view_key_keybase_proof.xml diff --git a/OpenKeychain/src/main/res/layout/view_key_keybase_proof.xml b/OpenKeychain/src/main/res/layout/view_key_keybase_proof.xml new file mode 100644 index 000000000..0ffd151c1 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/view_key_keybase_proof.xml @@ -0,0 +1,19 @@ + + + + + + From 3c19e6cfc12f6b24cf202aaaf9ad3e14223161d3 Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Fri, 7 Nov 2014 21:07:10 -0800 Subject: [PATCH 03/15] Fix a no-result corner case, and make verifications clickable --- .../sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java index ef6cd50f1..540dcc0b1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java @@ -284,7 +284,7 @@ public class ViewKeyTrustFragment extends LoaderFragment implements @Override protected void onPostExecute(ResultPage result) { super.onPostExecute(result); - if (result.mHeader == null) { + if (result.mProofs.isEmpty()) { result.mHeader = getActivity().getString(R.string.key_trust_no_cloud_evidence); } @@ -425,6 +425,7 @@ public class ViewKeyTrustFragment extends LoaderFragment implements } mProofVerifyHeader.setVisibility(View.VISIBLE); mProofVerifyDetail.setVisibility(View.VISIBLE); + mProofVerifyDetail.setMovementMethod(LinkMovementMethod.getInstance()); mProofVerifyDetail.setText(ssb); } } From 36bac67dd5f19b72a58584f2fab104e6e26df66a Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Tue, 11 Nov 2014 18:45:36 -0800 Subject: [PATCH 04/15] All keybase proofs now in place --- .../service/KeychainIntentService.java | 38 ++++++-- .../service/KeychainIntentServiceHandler.java | 5 ++ .../keychain/ui/ViewKeyTrustFragment.java | 90 +++++++++++-------- OpenKeychain/src/main/res/values/strings.xml | 7 ++ 4 files changed, 96 insertions(+), 44 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java index 42e0c7cc9..8a670df25 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -84,6 +84,12 @@ import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import de.measite.minidns.Client; +import de.measite.minidns.Question; +import de.measite.minidns.Record; +import de.measite.minidns.record.Data; +import de.measite.minidns.record.TXT; + /** * This Service contains all important long lasting operations for APG. It receives Intents with * data from the activities or other apps, queues these intents, executes them, and stops itself @@ -124,6 +130,7 @@ public class KeychainIntentService extends IntentService implements Progressable // encrypt, decrypt, import export public static final String TARGET = "target"; public static final String SOURCE = "source"; + // possible targets: public static final int IO_BYTES = 1; public static final int IO_URI = 2; @@ -321,12 +328,27 @@ public class KeychainIntentService extends IntentService implements Progressable return; } + String domain = prover.dnsTxtCheckRequired(); + if (domain != null) { + Record[] records = new Client().query(new Question(domain, Record.TYPE.TXT)).getAnswers(); + List> extents = new ArrayList>(); + for (Record r : records) { + Data d = r.getPayload(); + if (d instanceof TXT) { + extents.add(((TXT) d).getExtents()); + } + } + if (!prover.checkDnsTxt(extents)) { + sendProofError(prover.getLog(), null); + return; + } + } + byte[] messageBytes = prover.getPgpMessage().getBytes(); if (prover.rawMessageCheckRequired()) { InputStream messageByteStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(messageBytes)); - String problem = prover.checkRawMessageBytes(messageByteStream); - if (problem != null) { - sendProofError(prover.getLog(), problem); + if (!prover.checkRawMessageBytes(messageByteStream)) { + sendProofError(prover.getLog(), null); return; } } @@ -365,6 +387,11 @@ public class KeychainIntentService extends IntentService implements Progressable Bundle resultData = new Bundle(); resultData.putString(KeychainIntentServiceHandler.DATA_MESSAGE, "OK"); + + // these help the handler construct a useful human-readable message + resultData.putString(KeychainIntentServiceHandler.KEYBASE_PROOF_URL, prover.getProofUrl()); + resultData.putString(KeychainIntentServiceHandler.KEYBASE_PRESENCE_URL, prover.getPresenceUrl()); + resultData.putString(KeychainIntentServiceHandler.KEYBASE_PRESENCE_LABEL, prover.getPresenceLabel()); sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); } catch (Exception e) { sendErrorToHandler(e); @@ -678,11 +705,12 @@ public class KeychainIntentService extends IntentService implements Progressable private void sendProofError(List log, String label) { String msg = null; + label = (label == null) ? "" : label + ": "; for (String m : log) { - Log.e(Constants.TAG, label + ": " + m); + Log.e(Constants.TAG, label + m); msg = m; } - sendProofError(label + ": " + msg); + sendProofError(label + msg); } private void sendProofError(String msg) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java index 180020d0b..fc65757f5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java @@ -45,6 +45,11 @@ public class KeychainIntentServiceHandler extends Handler { public static final String DATA_MESSAGE = "message"; public static final String DATA_MESSAGE_ID = "message_id"; + // keybase proof specific + public static final String KEYBASE_PROOF_URL = "keybase_proof_url"; + public static final String KEYBASE_PRESENCE_URL = "keybase_presence_url"; + public static final String KEYBASE_PRESENCE_LABEL = "keybase_presence_label"; + Activity mActivity; ProgressDialogFragment mProgressDialogFragment; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java index 540dcc0b1..4965b2525 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java @@ -324,9 +324,6 @@ public class ViewKeyTrustFragment extends LoaderFragment implements } private void appendIfOK(Hashtable> table, Integer proofType, Proof proof) throws KeybaseException { - if (!proofIsOK(proof)) { - return; - } ArrayList list = table.get(proofType); if (list == null) { list = new ArrayList(); @@ -335,23 +332,16 @@ public class ViewKeyTrustFragment extends LoaderFragment implements list.add(proof); } - // We only accept http & https proofs. Maybe whitelist later? - private boolean proofIsOK(Proof proof) throws KeybaseException { - Uri uri = Uri.parse(proof.getServiceUrl()); - String scheme = uri.getScheme(); - return ("https".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme)); - } - // which proofs do we have working verifiers for? private boolean haveProofFor(int proofType) { switch (proofType) { case Proof.PROOF_TYPE_TWITTER: return true; case Proof.PROOF_TYPE_GITHUB: return true; - case Proof.PROOF_TYPE_DNS: return false; + case Proof.PROOF_TYPE_DNS: return true; case Proof.PROOF_TYPE_WEB_SITE: return true; case Proof.PROOF_TYPE_HACKERNEWS: return true; - case Proof.PROOF_TYPE_COINBASE: return false; - case Proof.PROOF_TYPE_REDDIT: return false; + case Proof.PROOF_TYPE_COINBASE: return true; + case Proof.PROOF_TYPE_REDDIT: return true; default: return false; } } @@ -381,47 +371,69 @@ public class ViewKeyTrustFragment extends LoaderFragment implements SpannableStringBuilder ssb = new SpannableStringBuilder(); if ((msg != null) && msg.equals("OK")) { - //yay - String serviceUrl, urlLabel, postUrl; - try { - serviceUrl = proof.getServiceUrl(); - if (serviceUrl.startsWith("https://")) { - urlLabel = serviceUrl.substring("https://".length()); - } else if (serviceUrl.startsWith("http://")) { - urlLabel = serviceUrl.substring("http://".length()); - } else { - urlLabel = serviceUrl; - } - postUrl = proof.getHumanUrl(); - } catch (KeybaseException e) { - throw new RuntimeException(e); + //yay + String proofUrl = returnData.getString(KeychainIntentServiceHandler.KEYBASE_PROOF_URL); + String presenceUrl = returnData.getString(KeychainIntentServiceHandler.KEYBASE_PRESENCE_URL); + String presenceLabel = returnData.getString(KeychainIntentServiceHandler.KEYBASE_PRESENCE_LABEL); + + String proofLabel; + switch (proof.getType()) { + case Proof.PROOF_TYPE_TWITTER: + proofLabel = getString(R.string.keybase_twitter_proof); + break; + case Proof.PROOF_TYPE_DNS: + proofLabel = getString(R.string.keybase_dns_proof); + break; + case Proof.PROOF_TYPE_WEB_SITE: + proofLabel = getString(R.string.keybase_web_site_proof); + break; + case Proof.PROOF_TYPE_GITHUB: + proofLabel = getString(R.string.keybase_github_proof); + break; + case Proof.PROOF_TYPE_REDDIT: + proofLabel = getString(R.string.keybase_reddit_proof); + break; + default: + proofLabel = getString(R.string.keybase_a_post); + break; } + ssb.append(getString(R.string.keybase_proof_succeeded)); StyleSpan bold = new StyleSpan(Typeface.BOLD); ssb.setSpan(bold, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); ssb.append("\n\n"); int length = ssb.length(); - String segment = getString(R.string.keybase_a_post); - ssb.append(segment); - URLSpan postLink = new URLSpan(postUrl); - ssb.setSpan(postLink, length, length + segment.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - ssb.append(" ").append(getString(R.string.keybase_fetched_from)).append(" "); - URLSpan serviceLink = new URLSpan(serviceUrl); + ssb.append(proofLabel); + if (proofUrl != null) { + URLSpan postLink = new URLSpan(proofUrl); + ssb.setSpan(postLink, length, length + proofLabel.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (Proof.PROOF_TYPE_DNS == proof.getType()) { + ssb.append(" ").append(getString(R.string.keybase_for_the_domain)).append(" "); + } else { + ssb.append(" ").append(getString(R.string.keybase_fetched_from)).append(" "); + } length = ssb.length(); - ssb.append(urlLabel); - ssb.setSpan(serviceLink, length, length + urlLabel.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + URLSpan presenceLink = new URLSpan(presenceUrl); + ssb.append(presenceLabel); + ssb.setSpan(presenceLink, length, length + presenceLabel.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + if (Proof.PROOF_TYPE_REDDIT == proof.getType()) { + ssb.append(", "). + append(getString(R.string.keybase_reddit_attribution)). + append(" “").append(proof.getHandle()).append("”, "); + } ssb.append(" ").append(getString(R.string.keybase_contained_signature)); - } else { + // verification failed! msg = returnData.getString(KeychainIntentServiceHandler.DATA_ERROR); ssb.append(getString(R.string.keybase_proof_failure)); if (msg == null) { msg = getString(R.string.keybase_unknown_proof_failure); - StyleSpan bold = new StyleSpan(Typeface.BOLD); - ssb.setSpan(bold, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - ssb.append("\n\n").append(msg); } + StyleSpan bold = new StyleSpan(Typeface.BOLD); + ssb.setSpan(bold, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.append("\n\n").append(msg); } mProofVerifyHeader.setVisibility(View.VISIBLE); mProofVerifyDetail.setVisibility(View.VISIBLE); diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index f582bd746..9596cccc1 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -567,7 +567,14 @@ "This proof has been verified!" "A post" "fetched from" + "for the domain" "contains a message which could only have been created by the owner of this key." + "A tweet" + "A DNS TXT record" + "A text file" + "A gist" + "A JSON file" + "attributed by Reddit to" "Change Passphrase" From ebd3876cbe91aa0514dab2f2ad34e7ec3cac354c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 18 Nov 2014 18:57:28 +0100 Subject: [PATCH 05/15] Update libs --- extern/KeybaseLib | 2 +- extern/minidns | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extern/KeybaseLib b/extern/KeybaseLib index 70c2f33e2..7f4980f07 160000 --- a/extern/KeybaseLib +++ b/extern/KeybaseLib @@ -1 +1 @@ -Subproject commit 70c2f33e26d3988a23524935810bcfe754b85a6f +Subproject commit 7f4980f0789d7bb3c2124080f32b3bec703c576e diff --git a/extern/minidns b/extern/minidns index 9e42bff01..118fefcaa 160000 --- a/extern/minidns +++ b/extern/minidns @@ -1 +1 @@ -Subproject commit 9e42bff01440c1351946a432126d5a1b87fb7c78 +Subproject commit 118fefcaaa44a7f31f5c18fa7e477f1665f654b6 From 6691f5118a1eb59d6a9334f44877f0bece1c1247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 18 Nov 2014 19:04:16 +0100 Subject: [PATCH 06/15] Version 3.2beta1 --- OpenKeychain/src/main/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 18fcc4b42..3af0bdf6c 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -3,8 +3,8 @@ xmlns:tools="http://schemas.android.com/tools" package="org.sufficientlysecure.keychain" android:installLocation="auto" - android:versionCode="31200" - android:versionName="3.1.2"> + android:versionCode="31201" + android:versionName="3.2beta1"> "Change Passphrase" From b5cdeb7f5a54be7443894ca2cb4bd27359fae9ce Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Wed, 19 Nov 2014 14:35:05 -0800 Subject: [PATCH 08/15] Prevent multiple cloud-trust-search launches. Handle DNS query failure gracefully. Fixes #1007 & #1008. --- .../keychain/service/KeychainIntentService.java | 8 +++++++- .../keychain/ui/ViewKeyTrustFragment.java | 1 + OpenKeychain/src/main/res/values/strings.xml | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java index 8a670df25..954963fb6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -85,6 +85,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import de.measite.minidns.Client; +import de.measite.minidns.DNSMessage; import de.measite.minidns.Question; import de.measite.minidns.Record; import de.measite.minidns.record.Data; @@ -330,7 +331,12 @@ public class KeychainIntentService extends IntentService implements Progressable String domain = prover.dnsTxtCheckRequired(); if (domain != null) { - Record[] records = new Client().query(new Question(domain, Record.TYPE.TXT)).getAnswers(); + DNSMessage dnsQuery = new Client().query(new Question(domain, Record.TYPE.TXT)); + if (dnsQuery == null) { + sendProofError(prover.getLog(), getString(R.string.keybase_dns_query_failure)); + return; + } + Record[] records = dnsQuery.getAnswers(); List> extents = new ArrayList>(); for (Record r : records) { Data d = r.getPayload(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java index ef14299b1..c85571493 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java @@ -190,6 +190,7 @@ public class ViewKeyTrustFragment extends LoaderFragment implements mStartSearch.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { + mStartSearch.setEnabled(false); new DescribeKey().execute(fingerprint); } }); diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index 27c81fe21..59c0ce408 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -561,6 +561,7 @@ "Unfortunately this proof cannot be verified." "Unrecognized problem with proof checker" "Problem with proof evidence" + "DNS TXT Record retrieval failed" "No proof checker found for" "Decrypted proof post does not match expected value" "Fetching proof evidence" From fd60d49d262a7920279a0f87060c7084069165e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sat, 22 Nov 2014 00:10:15 +0100 Subject: [PATCH 09/15] Use master key id for keybase proof verification --- .../keychain/pgp/PgpDecryptVerify.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java index 4161f2928..b094208a5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java @@ -274,17 +274,11 @@ public class PgpDecryptVerify extends BaseOperation { return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } - CanonicalizedPublicKey encryptKey = signingKey; - try { - encryptKey = signingRing.getEncryptionSubKey(); - } catch (PgpKeyNotFoundException e) { - } - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(signingKey.getFingerprint()); - String cryptFingerprint = KeyFormattingUtils.convertFingerprintToHex(encryptKey.getFingerprint()); - if (!(mRequiredSignerFingerprint.equals(fingerprint) || mRequiredSignerFingerprint.equals(cryptFingerprint))) { + String fingerprint = KeyFormattingUtils.convertFingerprintToHex(signingRing.getFingerprint()); + if (!(mRequiredSignerFingerprint.equals(fingerprint))) { log.add(LogType.MSG_VL_ERROR_MISSING_KEY, indent); - Log.d(Constants.TAG, "Key mismatch; wanted " + mRequiredSignerFingerprint + - " got " + fingerprint + "/" + cryptFingerprint); + Log.d(Constants.TAG, "Fingerprint mismatch; wanted " + mRequiredSignerFingerprint + + " got " + fingerprint + "!"); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } From bbbc45e4e9909806a91afe415265b507533f7556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sat, 22 Nov 2014 00:29:14 +0100 Subject: [PATCH 10/15] Dont accept signatures by expired or revoked subkeys --- .../pgp/OpenPgpSignatureResultBuilder.java | 4 ---- .../keychain/pgp/PgpDecryptVerify.java | 15 +++++++++++---- .../keychain/service/KeychainIntentService.java | 1 + .../keychain/ui/ViewKeyTrustFragment.java | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java index aa324c7ed..fc5064e79 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java @@ -84,10 +84,6 @@ public class OpenPgpSignatureResultBuilder { this.mUserIds = userIds; } - public boolean isValidSignature() { - return mValidSignature; - } - public void initValid(CanonicalizedPublicKeyRing signingRing, CanonicalizedPublicKey signingKey) { setSignatureAvailable(true); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java index b094208a5..ea9e165ba 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java @@ -22,6 +22,7 @@ import android.content.Context; import android.webkit.MimeTypeMap; import org.openintents.openpgp.OpenPgpMetadata; +import org.openintents.openpgp.OpenPgpSignatureResult; import org.spongycastle.bcpg.ArmoredInputStream; import org.spongycastle.openpgp.PGPCompressedData; import org.spongycastle.openpgp.PGPEncryptedData; @@ -332,7 +333,10 @@ public class PgpDecryptVerify extends BaseOperation { } signatureResultBuilder.setValidSignature(validSignature); - if (!signatureResultBuilder.isValidSignature()) { + OpenPgpSignatureResult signatureResult = signatureResultBuilder.build(); + + if (signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED + || signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED) { log.add(LogType.MSG_VL_ERROR_INTEGRITY_CHECK, indent); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } @@ -344,7 +348,7 @@ public class PgpDecryptVerify extends BaseOperation { // Return a positive result, with metadata and verification info DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); - result.setSignatureResult(signatureResultBuilder.build()); + result.setSignatureResult(signatureResult); return result; } @@ -773,6 +777,8 @@ public class PgpDecryptVerify extends BaseOperation { metadata = null; } + OpenPgpSignatureResult signatureResult = signatureResultBuilder.build(); + if (encryptedData.isIntegrityProtected()) { updateProgress(R.string.progress_verifying_integrity, 95, 100); @@ -786,7 +792,8 @@ public class PgpDecryptVerify extends BaseOperation { // If no valid signature is present: // Handle missing integrity protection like failed integrity protection! // The MDC packet can be stripped by an attacker! - if (!signatureResultBuilder.isValidSignature()) { + if (signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED + || signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED) { log.add(LogType.MSG_DC_ERROR_INTEGRITY_CHECK, indent); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } @@ -800,7 +807,7 @@ public class PgpDecryptVerify extends BaseOperation { DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); result.setDecryptMetadata(metadata); - result.setSignatureResult(signatureResultBuilder.build()); + result.setSignatureResult(signatureResult); return result; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java index 92c64a4e1..a4a3a801a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -30,6 +30,7 @@ import com.textuality.keybase.lib.Proof; import com.textuality.keybase.lib.prover.Prover; import org.json.JSONObject; +import org.openintents.openpgp.OpenPgpSignatureResult; import org.spongycastle.openpgp.PGPUtil; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java index c85571493..677646441 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java @@ -362,7 +362,7 @@ public class ViewKeyTrustFragment extends LoaderFragment implements // Create a new Messenger for the communication back after proof work is done // KeychainIntentServiceHandler handler = new KeychainIntentServiceHandler(getActivity(), - getString(R.string.progress_decrypting), ProgressDialog.STYLE_HORIZONTAL) { + getString(R.string.progress_verifying_signature), ProgressDialog.STYLE_HORIZONTAL) { public void handleMessage(Message message) { // handle messages by standard KeychainIntentServiceHandler first super.handleMessage(message); From 9c133d343fbc297ed6f3ee39b74cea5dfcc9c207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sat, 22 Nov 2014 02:55:42 +0100 Subject: [PATCH 11/15] fix signature check --- .../org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java index ea9e165ba..5589a3521 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java @@ -336,7 +336,7 @@ public class PgpDecryptVerify extends BaseOperation { OpenPgpSignatureResult signatureResult = signatureResultBuilder.build(); if (signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED - || signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED) { + && signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED) { log.add(LogType.MSG_VL_ERROR_INTEGRITY_CHECK, indent); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } @@ -793,7 +793,7 @@ public class PgpDecryptVerify extends BaseOperation { // Handle missing integrity protection like failed integrity protection! // The MDC packet can be stripped by an attacker! if (signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED - || signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED) { + && signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED) { log.add(LogType.MSG_DC_ERROR_INTEGRITY_CHECK, indent); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } From e72c082acd9f17be4a21970603df0f6a621221d7 Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Fri, 21 Nov 2014 19:44:05 -0800 Subject: [PATCH 12/15] Add check that proof & database fingerprints are the same --- .../keychain/pgp/PgpDecryptVerify.java | 3 ++- .../keychain/service/KeychainIntentService.java | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java index ea9e165ba..5a8bfda29 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java @@ -336,7 +336,8 @@ public class PgpDecryptVerify extends BaseOperation { OpenPgpSignatureResult signatureResult = signatureResultBuilder.build(); if (signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED - || signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED) { + && signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED) { + Log.d(Constants.TAG, "STATUS IS " + signatureResult.getStatus()); log.add(LogType.MSG_VL_ERROR_INTEGRITY_CHECK, indent); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java index a4a3a801a..dc9592710 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -30,7 +30,6 @@ import com.textuality.keybase.lib.Proof; import com.textuality.keybase.lib.prover.Prover; import org.json.JSONObject; -import org.openintents.openpgp.OpenPgpSignatureResult; import org.spongycastle.openpgp.PGPUtil; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; @@ -324,6 +323,11 @@ public class KeychainIntentService extends IntentService implements Progressable sendProofError(prover.getLog(), getString(R.string.keybase_problem_fetching_evidence)); return; } + String requiredFingerprint = data.getString(KEYBASE_REQUIRED_FINGERPRINT); + if (!prover.checkFingerprint(requiredFingerprint)) { + sendProofError(getString(R.string.keybase_key_mismatch)); + return; + } String domain = prover.dnsTxtCheckRequired(); if (domain != null) { @@ -361,13 +365,12 @@ public class KeychainIntentService extends IntentService implements Progressable InputData inputData = createDecryptInputData(data); OutputStream outStream = createCryptOutputStream(data); - String fingerprint = data.getString(KEYBASE_REQUIRED_FINGERPRINT); PgpDecryptVerify.Builder builder = new PgpDecryptVerify.Builder( this, new ProviderHelper(this), this, inputData, outStream ); - builder.setSignedLiteralData(true).setRequiredSignerFingerprint(fingerprint); + builder.setSignedLiteralData(true).setRequiredSignerFingerprint(requiredFingerprint); DecryptVerifyResult decryptVerifyResult = builder.build().execute(); outStream.close(); From 2a608c12ca592c8662586898a161e7054fc0693c Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Sat, 22 Nov 2014 09:27:28 -0800 Subject: [PATCH 13/15] Check fingerprint match between proof & database --- OpenKeychain/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index b98f47146..cac38c361 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -562,6 +562,7 @@ "Unfortunately this proof cannot be verified." "Unrecognized problem with proof checker" "Problem with proof evidence" + "Key fingerprint doesn’t match that in proof post" "DNS TXT Record retrieval failed" "No proof checker found for" "Decrypted proof post does not match expected value" From be694f62437343b46e2edadccf22b399f234c657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Thu, 4 Dec 2014 18:57:09 +0100 Subject: [PATCH 14/15] Update for Android Studio 1.0 RC --- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.jar | Bin 50557 -> 51018 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 2e7ab3d86..59f05e73a 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { dependencies { // NOTE: Always use fixed version codes not dynamic ones, e.g. 0.7.3 instead of 0.7.+, see README for more information - classpath 'com.android.tools.build:gradle:0.12.2' + classpath 'com.android.tools.build:gradle:1.0.0-rc3' } } @@ -16,7 +16,7 @@ allprojects { } task wrapper(type: Wrapper) { - gradleVersion = '1.12' + gradleVersion = '2.2.1' } subprojects { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5838598129719e795cc5633f411468bdec311016..c97a8bdb9088d370da7e88784a7a093b971aa23a 100644 GIT binary patch delta 25841 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 delta 25388 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cc3d5d9a3..120a028f4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jun 09 22:04:23 CEST 2014 +#Thu Dec 04 18:50:40 CET 2014 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-bin.zip From 07619b536a78002194928490e017dcc2d19e9644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Thu, 4 Dec 2014 19:02:50 +0100 Subject: [PATCH 15/15] Update for Android Studio 1.0 RC --- extern/KeybaseLib | 2 +- extern/StickyListHeaders | 2 +- extern/SuperToasts | 2 +- extern/TokenAutoComplete | 2 +- extern/minidns | 2 +- extern/openkeychain-api-lib | 2 +- extern/openpgp-api-lib | 2 +- extern/zxing-android-integration | 2 +- extern/zxing-qr-code | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extern/KeybaseLib b/extern/KeybaseLib index 7f4980f07..2b26d163d 160000 --- a/extern/KeybaseLib +++ b/extern/KeybaseLib @@ -1 +1 @@ -Subproject commit 7f4980f0789d7bb3c2124080f32b3bec703c576e +Subproject commit 2b26d163df84a3d26c1c8da088ed3811b5ca6ec7 diff --git a/extern/StickyListHeaders b/extern/StickyListHeaders index 911f8ddfd..62cc58c12 160000 --- a/extern/StickyListHeaders +++ b/extern/StickyListHeaders @@ -1 +1 @@ -Subproject commit 911f8ddfd007ce65aededae7e7b79e5a8d903a43 +Subproject commit 62cc58c12d0c09b50984caf26e5afceda8873784 diff --git a/extern/SuperToasts b/extern/SuperToasts index 8578cfe69..77042d633 160000 --- a/extern/SuperToasts +++ b/extern/SuperToasts @@ -1 +1 @@ -Subproject commit 8578cfe6917cf16a9f123c1964e4bbff2a15be59 +Subproject commit 77042d633f4dd430bcc86101e31dda52433db9c1 diff --git a/extern/TokenAutoComplete b/extern/TokenAutoComplete index 000fa6539..ca46b4261 160000 --- a/extern/TokenAutoComplete +++ b/extern/TokenAutoComplete @@ -1 +1 @@ -Subproject commit 000fa65390b98106201a0ffaad76c5d85a35396c +Subproject commit ca46b4261c97221ddd4db135e7838d6fa145adf4 diff --git a/extern/minidns b/extern/minidns index 118fefcaa..f3a19080f 160000 --- a/extern/minidns +++ b/extern/minidns @@ -1 +1 @@ -Subproject commit 118fefcaaa44a7f31f5c18fa7e477f1665f654b6 +Subproject commit f3a19080f15e220fbacab5045c1f15fd12513b35 diff --git a/extern/openkeychain-api-lib b/extern/openkeychain-api-lib index 57e58e7f7..0cdbf3223 160000 --- a/extern/openkeychain-api-lib +++ b/extern/openkeychain-api-lib @@ -1 +1 @@ -Subproject commit 57e58e7f7f51c4eecd7ee4b0f22c856485a243b7 +Subproject commit 0cdbf32231739eac47999be71d6d01fc28375181 diff --git a/extern/openpgp-api-lib b/extern/openpgp-api-lib index 0be263d5d..e0ad1086a 160000 --- a/extern/openpgp-api-lib +++ b/extern/openpgp-api-lib @@ -1 +1 @@ -Subproject commit 0be263d5d3effd2df5f976fa4a127017268749cc +Subproject commit e0ad1086a55eab66d963b4d5c6ca5544b454ef2d diff --git a/extern/zxing-android-integration b/extern/zxing-android-integration index 1d7878456..e2d0064bd 160000 --- a/extern/zxing-android-integration +++ b/extern/zxing-android-integration @@ -1 +1 @@ -Subproject commit 1d787845663fd232f98f5e8e0923733c1a188f2a +Subproject commit e2d0064bd3171b7333af044bb30c25c85ee993dd diff --git a/extern/zxing-qr-code b/extern/zxing-qr-code index 9ef2f3b66..8fd0657d3 160000 --- a/extern/zxing-qr-code +++ b/extern/zxing-qr-code @@ -1 +1 @@ -Subproject commit 9ef2f3b66ea7cc283e865ec39434d023a18d17f3 +Subproject commit 8fd0657d33d8277aadbdc1604fd3aaa2e8d4b487