From c05441667e151dceb6f5874b290d70a53258061b Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Fri, 7 Nov 2014 12:28:27 -0800 Subject: [PATCH 01/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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 From d7888d46668a68a138743e30c64be45b35b5211a Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Thu, 26 Feb 2015 18:52:54 +0100 Subject: [PATCH 16/26] ignore revoked user ids for primary key expiry --- .../keychain/pgp/CanonicalizedKeyRing.java | 5 +- .../keychain/pgp/CanonicalizedPublicKey.java | 74 +++++++++++++++++++ .../keychain/pgp/PgpKeyOperation.java | 4 +- .../keychain/pgp/UncachedPublicKey.java | 18 ----- .../keychain/pgp/WrappedSignature.java | 4 + 5 files changed, 82 insertions(+), 23 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java index bbf136dac..4adacaf23 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java @@ -79,9 +79,8 @@ public abstract class CanonicalizedKeyRing extends KeyRing { public boolean isExpired() { // Is the master key expired? - Date creationDate = getRing().getPublicKey().getCreationTime(); - Date expiryDate = getRing().getPublicKey().getValidSeconds() > 0 - ? new Date(creationDate.getTime() + getRing().getPublicKey().getValidSeconds() * 1000) : null; + Date creationDate = getPublicKey().getCreationTime(); + Date expiryDate = getPublicKey().getExpiryTime(); Date now = new Date(); return creationDate.after(now) || (expiryDate != null && expiryDate.before(now)); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKey.java index b026d9257..303070333 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKey.java @@ -20,8 +20,16 @@ package org.sufficientlysecure.keychain.pgp; import org.spongycastle.bcpg.sig.KeyFlags; import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPSignature; import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; +import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.util.IterableIterator; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Iterator; /** Wrapper for a PGPPublicKey. * @@ -100,6 +108,72 @@ public class CanonicalizedPublicKey extends UncachedPublicKey { return false; } + public boolean isRevoked() { + return mPublicKey.getSignaturesOfType(isMasterKey() + ? PGPSignature.KEY_REVOCATION + : PGPSignature.SUBKEY_REVOCATION).hasNext(); + } + + public boolean isExpired () { + Date expiry = getExpiryTime(); + return expiry != null && expiry.before(new Date()); + } + + public long getValidSeconds() { + + long seconds; + + // the getValidSeconds method is unreliable for master keys. we need to iterate all + // user ids, then use the most recent certification from a non-revoked user id + if (isMasterKey()) { + Date latestCreation = null; + seconds = 0; + + for (byte[] rawUserId : getUnorderedRawUserIds()) { + Iterator sigs = getSignaturesForRawId(rawUserId); + + // there is always a certification, so this call is safe + WrappedSignature sig = sigs.next(); + + // we know a user id has at most two sigs: one certification, one revocation. + // if the sig is a revocation, or there is another sig (which is a revocation), + // the data in this uid is not relevant + if (sig.isRevocation() || sigs.hasNext()) { + continue; + } + + // this is our revocation, UNLESS there is a newer certificate! + if (latestCreation == null || latestCreation.before(sig.getCreationTime())) { + latestCreation = sig.getCreationTime(); + seconds = sig.getKeyExpirySeconds(); + } + } + } else { + seconds = mPublicKey.getValidSeconds(); + } + + return seconds; + } + + public Date getExpiryTime() { + long seconds = getValidSeconds(); + + if (seconds > Integer.MAX_VALUE) { + Log.e(Constants.TAG, "error, expiry time too large"); + return null; + } + if (seconds == 0) { + // no expiry + return null; + } + Date creationDate = getCreationTime(); + Calendar calendar = GregorianCalendar.getInstance(); + calendar.setTime(creationDate); + calendar.add(Calendar.SECOND, (int) seconds); + + return calendar.getTime(); + } + /** Same method as superclass, but we make it public. */ public Integer getKeyUsage() { return super.getKeyUsage(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java index aebb52a03..1a251eb79 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java @@ -439,8 +439,8 @@ public class PgpKeyOperation { // since this is the master key, this contains at least CERTIFY_OTHER PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey(); int masterKeyFlags = readKeyFlags(masterPublicKey) | KeyFlags.CERTIFY_OTHER; - long masterKeyExpiry = masterPublicKey.getValidSeconds() == 0L ? 0L : - masterPublicKey.getCreationTime().getTime() / 1000 + masterPublicKey.getValidSeconds(); + Date expiryTime = wsKR.getPublicKey().getExpiryTime(); + long masterKeyExpiry = expiryTime != null ? expiryTime.getTime() / 1000 : 0L; return internal(sKR, masterSecretKey, masterKeyFlags, masterKeyExpiry, saveParcel, passphrase, log); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java index 0fe1ccdb6..d29169cc4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java @@ -60,24 +60,6 @@ public class UncachedPublicKey { return mPublicKey.getCreationTime(); } - public Date getExpiryTime() { - long seconds = mPublicKey.getValidSeconds(); - if (seconds > Integer.MAX_VALUE) { - Log.e(Constants.TAG, "error, expiry time too large"); - return null; - } - if (seconds == 0) { - // no expiry - return null; - } - Date creationDate = getCreationTime(); - Calendar calendar = GregorianCalendar.getInstance(); - calendar.setTime(creationDate); - calendar.add(Calendar.SECOND, (int) seconds); - - return calendar.getTime(); - } - public boolean isExpired() { Date creationDate = mPublicKey.getCreationTime(); Date expiryDate = mPublicKey.getValidSeconds() > 0 diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSignature.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSignature.java index ade075d55..c6fad1a73 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSignature.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSignature.java @@ -78,6 +78,10 @@ public class WrappedSignature { return mSig.getCreationTime(); } + public long getKeyExpirySeconds() { + return mSig.getHashedSubPackets().getKeyExpirationTime(); + } + public ArrayList getEmbeddedSignatures() { ArrayList sigs = new ArrayList<>(); if (!mSig.hasSubpackets()) { From e5bb7a35b5202cf8ef13325d86ef82f2583700b7 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Thu, 26 Feb 2015 18:53:16 +0100 Subject: [PATCH 17/26] save revocation instead of self-cert for revoked uids --- .../keychain/provider/ProviderHelper.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java index 18efa2b80..d947ae053 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java @@ -473,7 +473,7 @@ public class ProviderHelper { item.selfCert = cert; item.isPrimary = cert.isPrimaryUserId(); } else { - item.isRevoked = true; + item.selfRevocation = cert; log(LogType.MSG_IP_UID_REVOKED); } continue; @@ -569,10 +569,11 @@ public class ProviderHelper { // NOTE self-certificates are already verified during canonicalization, // AND we know there is at most one cert plus at most one revocation + // AND the revocation only exists if there is no newer certification if (!cert.isRevocation()) { item.selfCert = cert; } else { - item.isRevoked = true; + item.selfRevocation = cert; log(LogType.MSG_IP_UAT_REVOKED); } continue; @@ -643,16 +644,21 @@ public class ProviderHelper { for (int userIdRank = 0; userIdRank < uids.size(); userIdRank++) { UserPacketItem item = uids.get(userIdRank); operations.add(buildUserIdOperations(masterKeyId, item, userIdRank)); - if (item.selfCert != null) { - // TODO get rid of "self verified" status? this cannot even happen anymore! - operations.add(buildCertOperations(masterKeyId, userIdRank, item.selfCert, - selfCertsAreTrusted ? Certs.VERIFIED_SECRET : Certs.VERIFIED_SELF)); + + if (item.selfCert == null) { + throw new AssertionError("User ids MUST be self-certified at this point!!"); } - // don't bother with trusted certs if the uid is revoked, anyways - if (item.isRevoked) { + + if (item.selfRevocation != null) { + operations.add(buildCertOperations(masterKeyId, userIdRank, item.selfRevocation, + Certs.VERIFIED_SELF)); + // don't bother with trusted certs if the uid is revoked, anyways continue; } + operations.add(buildCertOperations(masterKeyId, userIdRank, item.selfCert, + selfCertsAreTrusted ? Certs.VERIFIED_SECRET : Certs.VERIFIED_SELF)); + // iterate over signatures for (int i = 0; i < item.trustedCerts.size() ; i++) { WrappedSignature sig = item.trustedCerts.valueAt(i); @@ -711,15 +717,16 @@ public class ProviderHelper { String userId; byte[] attributeData; boolean isPrimary = false; - boolean isRevoked = false; WrappedSignature selfCert; + WrappedSignature selfRevocation; LongSparseArray trustedCerts = new LongSparseArray<>(); @Override public int compareTo(UserPacketItem o) { // revoked keys always come last! - if (isRevoked != o.isRevoked) { - return isRevoked ? 1 : -1; + //noinspection DoubleNegation + if ( (selfRevocation != null) != (o.selfRevocation != null)) { + return selfRevocation != null ? 1 : -1; } // if one is a user id, but the other isn't, the user id always comes first. // we compare for null values here, so != is the correct operator! @@ -1353,7 +1360,7 @@ public class ProviderHelper { values.put(UserPackets.USER_ID, item.userId); values.put(UserPackets.ATTRIBUTE_DATA, item.attributeData); values.put(UserPackets.IS_PRIMARY, item.isPrimary); - values.put(UserPackets.IS_REVOKED, item.isRevoked); + values.put(UserPackets.IS_REVOKED, item.selfRevocation != null); values.put(UserPackets.RANK, rank); Uri uri = UserPackets.buildUserIdsUri(masterKeyId); From 55dd6526a607c35ac31e56e1e26deb151b950218 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Thu, 26 Feb 2015 18:53:42 +0100 Subject: [PATCH 18/26] split up and mark unsafe expiry-related methods --- .../keychain/pgp/PgpKeyOperationTest.java | 40 +++++++++---------- .../keyimport/ImportKeysListEntry.java | 4 +- .../pgp/OpenPgpSignatureResultBuilder.java | 4 +- .../keychain/pgp/UncachedPublicKey.java | 25 +++++++++++- 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java index 0288d2937..1da69308c 100644 --- a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java @@ -231,7 +231,7 @@ public class PgpKeyOperationTest { ring.getPublicKey().getCreationTime().after(new Date(new Date().getTime()-1000*120))); Assert.assertNull("key ring should not expire", - ring.getPublicKey().getExpiryTime()); + ring.getPublicKey().getUnsafeExpiryTimeForTesting()); Assert.assertEquals("first (master) key can certify", KeyFlags.CERTIFY_OTHER, (long) subkeys.get(0).getKeyUsage()); @@ -342,9 +342,9 @@ public class PgpKeyOperationTest { Assert.assertNotNull("new key is not null", newKey); Assert.assertNotNull("added key must have an expiry date", - newKey.getExpiryTime()); + newKey.getUnsafeExpiryTimeForTesting()); Assert.assertEquals("added key must have expected expiry date", - expiry, newKey.getExpiryTime().getTime()/1000); + expiry, newKey.getUnsafeExpiryTimeForTesting().getTime()/1000); Assert.assertEquals("added key must have expected flags", flags, (long) newKey.getKeyUsage()); Assert.assertEquals("added key must have expected bitsize", @@ -403,9 +403,9 @@ public class PgpKeyOperationTest { ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID()); Assert.assertNotNull("modified key must have an expiry date", - modified.getPublicKey(keyId).getExpiryTime()); + modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); Assert.assertEquals("modified key must have expected expiry date", - expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000); + expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000); Assert.assertEquals("modified key must have same flags as before", ring.getPublicKey(keyId).getKeyUsage(), modified.getPublicKey(keyId).getKeyUsage()); } @@ -417,9 +417,9 @@ public class PgpKeyOperationTest { modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB); Assert.assertNotNull("modified key must have an expiry date", - modified.getPublicKey(keyId).getExpiryTime()); + modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); Assert.assertEquals("modified key must have expected expiry date", - expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000); + expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000); Assert.assertEquals("modified key must have same flags as before", ring.getPublicKey(keyId).getKeyUsage(), modified.getPublicKey(keyId).getKeyUsage()); } @@ -443,9 +443,9 @@ public class PgpKeyOperationTest { Assert.assertEquals("modified key must have expected flags", flags, (long) modified.getPublicKey(keyId).getKeyUsage()); Assert.assertNotNull("key must retain its expiry", - modified.getPublicKey(keyId).getExpiryTime()); + modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); Assert.assertEquals("key expiry must be unchanged", - expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000); + expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000); } { // expiry of 0 should be "no expiry" @@ -463,7 +463,7 @@ public class PgpKeyOperationTest { Assert.assertEquals("signature must have been created by master key", ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID()); - Assert.assertNull("key must not expire anymore", modified.getPublicKey(keyId).getExpiryTime()); + Assert.assertNull("key must not expire anymore", modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); } { // a past expiry should fail @@ -517,9 +517,9 @@ public class PgpKeyOperationTest { PacketTags.SIGNATURE, onlyB.get(1).tag); Assert.assertNotNull("modified key must have an expiry date", - modified.getPublicKey().getExpiryTime()); + modified.getPublicKey().getUnsafeExpiryTimeForTesting()); Assert.assertEquals("modified key must have expected expiry date", - expiry, modified.getPublicKey().getExpiryTime().getTime() / 1000); + expiry, modified.getPublicKey().getUnsafeExpiryTimeForTesting().getTime() / 1000); Assert.assertEquals("modified key must have same flags as before", ring.getPublicKey().getKeyUsage(), modified.getPublicKey().getKeyUsage()); } @@ -531,9 +531,9 @@ public class PgpKeyOperationTest { modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB); Assert.assertNotNull("modified key must have an expiry date", - modified.getPublicKey(keyId).getExpiryTime()); + modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); Assert.assertEquals("modified key must have expected expiry date", - expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000); + expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000); Assert.assertEquals("modified key must have same flags as before", ring.getPublicKey(keyId).getKeyUsage(), modified.getPublicKey(keyId).getKeyUsage()); } @@ -547,9 +547,9 @@ public class PgpKeyOperationTest { Assert.assertEquals("modified key must have expected flags", flags, (long) modified.getPublicKey(keyId).getKeyUsage()); Assert.assertNotNull("key must retain its expiry", - modified.getPublicKey(keyId).getExpiryTime()); + modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); Assert.assertEquals("key expiry must be unchanged", - expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000); + expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000); } { // expiry of 0 should be "no expiry" @@ -557,7 +557,7 @@ public class PgpKeyOperationTest { parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, 0L)); modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB); - Assert.assertNull("key must not expire anymore", modified.getPublicKey(keyId).getExpiryTime()); + Assert.assertNull("key must not expire anymore", modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); } { // if we revoke everything, nothing is left to properly sign... @@ -609,7 +609,7 @@ public class PgpKeyOperationTest { ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID()); Assert.assertTrue("subkey must actually be revoked", - modified.getPublicKey().isRevoked()); + modified.getPublicKey().isMaybeRevoked()); } @@ -653,7 +653,7 @@ public class PgpKeyOperationTest { ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID()); Assert.assertTrue("subkey must actually be revoked", - modified.getPublicKey(keyId).isRevoked()); + modified.getPublicKey(keyId).isMaybeRevoked()); } { // re-add second subkey @@ -691,7 +691,7 @@ public class PgpKeyOperationTest { ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID()); Assert.assertFalse("subkey must no longer be revoked", - modified.getPublicKey(keyId).isRevoked()); + modified.getPublicKey(keyId).isMaybeRevoked()); Assert.assertEquals("subkey must have the same usage flags as before", flags, (long) modified.getPublicKey(keyId).getKeyUsage()); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java index 591408c8b..79065604a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java @@ -294,8 +294,8 @@ public class ImportKeysListEntry implements Serializable, Parcelable { mKeyId = key.getKeyId(); mKeyIdHex = KeyFormattingUtils.convertKeyIdToHex(mKeyId); - mRevoked = key.isRevoked(); - mExpired = key.isExpired(); + mRevoked = key.isMaybeRevoked(); + mExpired = key.isMaybeExpired(); mFingerprintHex = KeyFormattingUtils.convertFingerprintToHex(key.getFingerprint()); mBitStrength = key.getBitStrength(); mCurveOid = key.getCurveOid(); 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 ed4715681..46defebf7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java @@ -104,8 +104,8 @@ public class OpenPgpSignatureResultBuilder { setUserIds(signingRing.getUnorderedUserIds()); // either master key is expired/revoked or this specific subkey is expired/revoked - setKeyExpired(signingRing.isExpired() || signingKey.isExpired()); - setKeyRevoked(signingRing.isRevoked() || signingKey.isRevoked()); + setKeyExpired(signingRing.isExpired() || signingKey.isMaybeExpired()); + setKeyRevoked(signingRing.isRevoked() || signingKey.isMaybeRevoked()); } public OpenPgpSignatureResult build() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java index d29169cc4..9276cba10 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java @@ -50,7 +50,7 @@ public class UncachedPublicKey { } /** The revocation signature is NOT checked here, so this may be false! */ - public boolean isRevoked() { + public boolean isMaybeRevoked() { return mPublicKey.getSignaturesOfType(isMasterKey() ? PGPSignature.KEY_REVOCATION : PGPSignature.SUBKEY_REVOCATION).hasNext(); @@ -60,7 +60,8 @@ public class UncachedPublicKey { return mPublicKey.getCreationTime(); } - public boolean isExpired() { + /** The revocation signature is NOT checked here, so this may be false! */ + public boolean isMaybeExpired() { Date creationDate = mPublicKey.getCreationTime(); Date expiryDate = mPublicKey.getValidSeconds() > 0 ? new Date(creationDate.getTime() + mPublicKey.getValidSeconds() * 1000) : null; @@ -340,4 +341,24 @@ public class UncachedPublicKey { return mCacheUsage; } + // this method relies on UNSAFE assumptions about the keyring, and should ONLY be used for + // TEST CASES!! + Date getUnsafeExpiryTimeForTesting () { + long valid = mPublicKey.getValidSeconds(); + + if (valid > Integer.MAX_VALUE) { + Log.e(Constants.TAG, "error, expiry time too large"); + return null; + } + if (valid == 0) { + // no expiry + return null; + } + Date creationDate = getCreationTime(); + Calendar calendar = GregorianCalendar.getInstance(); + calendar.setTime(creationDate); + calendar.add(Calendar.SECOND, (int) valid); + + return calendar.getTime(); + } } From a70d80483df4576d8d02fccde73ac6defa55a1f9 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Thu, 26 Feb 2015 19:06:23 +0100 Subject: [PATCH 19/26] add unit test for "no expiry where revoked user id still has expiry" case --- .../keychain/pgp/PgpKeyOperationTest.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java index 1da69308c..dd2feb825 100644 --- a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java @@ -533,7 +533,7 @@ public class PgpKeyOperationTest { Assert.assertNotNull("modified key must have an expiry date", modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); Assert.assertEquals("modified key must have expected expiry date", - expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000); + expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime() / 1000); Assert.assertEquals("modified key must have same flags as before", ring.getPublicKey(keyId).getKeyUsage(), modified.getPublicKey(keyId).getKeyUsage()); } @@ -553,11 +553,23 @@ public class PgpKeyOperationTest { } { // expiry of 0 should be "no expiry" + + // even if there is a non-expiring user id while all others are revoked, it doesn't count! + // for this purpose we revoke one while they still have expiry times + parcel.reset(); + parcel.mRevokeUserIds.add("aloe"); + modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB); + parcel.reset(); parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, 0L)); modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB); - Assert.assertNull("key must not expire anymore", modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); + // for this check, it is relevant that we DON'T use the unsafe one! + Assert.assertNull("key must not expire anymore", + modified.canonicalize(new OperationLog(), 0).getPublicKey().getExpiryTime()); + // make sure the unsafe one behaves incorrectly as expected + Assert.assertNotNull("unsafe expiry must yield wrong result from revoked user id", + modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting()); } { // if we revoke everything, nothing is left to properly sign... From 6bd4e1e5b377225e55491b77f5d495a7661ad5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Fri, 27 Feb 2015 01:26:04 +0100 Subject: [PATCH 20/26] Fix margins of dialog views --- .../src/main/res/layout/add_subkey_dialog.xml | 8 ++++---- .../src/main/res/layout/add_user_id_dialog.xml | 8 ++++---- .../res/layout/edit_subkey_expiry_dialog.xml | 7 ++++--- .../src/main/res/layout/file_dialog.xml | 17 ++++++++++------- .../res/layout/passphrase_repeat_dialog.xml | 6 ++++-- .../res/layout/view_key_delete_fragment.xml | 5 ++++- 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/OpenKeychain/src/main/res/layout/add_subkey_dialog.xml b/OpenKeychain/src/main/res/layout/add_subkey_dialog.xml index d52e72997..d32b1496f 100644 --- a/OpenKeychain/src/main/res/layout/add_subkey_dialog.xml +++ b/OpenKeychain/src/main/res/layout/add_subkey_dialog.xml @@ -6,10 +6,10 @@ + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:paddingLeft="24dp" + android:paddingRight="24dp"> + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:paddingLeft="24dp" + android:paddingRight="24dp"> + android:lines="1" + android:maxLines="1" + android:minLines="1" + android:scrollbars="vertical" + android:layout_gravity="center_vertical" /> + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:paddingLeft="24dp" + android:paddingRight="24dp"> @@ -8,7 +12,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/mainMessage" - android:layout_margin="4dp" android:textAppearance="?android:textAppearanceMedium" /> \ No newline at end of file From 8230fb11799fb2476edafe834b15de56ec6ee112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Fri, 27 Feb 2015 01:26:26 +0100 Subject: [PATCH 21/26] Remove unused BadImportKeyDialogFragment --- .../ui/dialog/BadImportKeyDialogFragment.java | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/BadImportKeyDialogFragment.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/BadImportKeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/BadImportKeyDialogFragment.java deleted file mode 100644 index 19cf27259..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/BadImportKeyDialogFragment.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2012-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.dialog; - -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.FragmentActivity; - -import org.sufficientlysecure.keychain.R; - -public class BadImportKeyDialogFragment extends DialogFragment { - private static final String ARG_BAD_IMPORT = "bad_import"; - - /** - * Creates a new instance of this Bad Import Key DialogFragment - * - * @param bad - * @return - */ - public static BadImportKeyDialogFragment newInstance(int bad) { - BadImportKeyDialogFragment frag = new BadImportKeyDialogFragment(); - Bundle args = new Bundle(); - - args.putInt(ARG_BAD_IMPORT, bad); - frag.setArguments(args); - - return frag; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final FragmentActivity activity = getActivity(); - final int badImport = getArguments().getInt(ARG_BAD_IMPORT); - - CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(activity); - alert.setIcon(R.drawable.ic_dialog_alert_holo_light); - alert.setTitle(R.string.warning); - alert.setMessage(activity.getResources() - .getQuantityString(R.plurals.bad_keys_encountered, badImport, badImport)); - alert.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); - alert.setCancelable(true); - - return alert.show(); - } -} From b90335f901c66424dcfc9d1790495357700509b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Fri, 27 Feb 2015 01:30:32 +0100 Subject: [PATCH 22/26] Simplify delete key dialog --- .../keychain/ui/ViewKeyActivity.java | 2 -- .../keychain/ui/dialog/DeleteKeyDialogFragment.java | 11 +++++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) 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 e1a8981c4..5c7a4448b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -184,7 +184,6 @@ public class ViewKeyActivity extends BaseActivity implements } }); - // Prepare the loaders. Either re-connect with an existing ones, // or start new ones. getSupportLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); @@ -447,7 +446,6 @@ public class ViewKeyActivity extends BaseActivity implements startActivityForResult(safeSlingerIntent, 0); } - /** * Load QR Code asynchronously and with a fade in animation * diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java index 5b96ea231..802f0c11b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java @@ -33,6 +33,7 @@ import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.service.KeychainIntentService; @@ -100,14 +101,20 @@ public class DeleteKeyDialogFragment extends DialogFragment { ProviderHelper.FIELD_TYPE_INTEGER } ); - String userId = (String) data.get(KeyRings.USER_ID); + String name; + String[] mainUserId = KeyRing.splitUserId((String) data.get(KeyRings.USER_ID)); + if (mainUserId[0] != null) { + name = mainUserId[0]; + } else { + name = getString(R.string.user_id_no_name); + } hasSecret = ((Long) data.get(KeyRings.HAS_ANY_SECRET)) == 1; // Set message depending on which key it is. mMainMessage.setText(getString( hasSecret ? R.string.secret_key_deletion_confirmation : R.string.public_key_deletetion_confirmation, - userId + name )); } catch (ProviderHelper.NotFoundException e) { dismiss(); From 4ccd9f9bb148a75e9cde48648216d1037a617c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Fri, 27 Feb 2015 01:38:57 +0100 Subject: [PATCH 23/26] Uncluttering advanced key view --- .../keychain/ui/ViewKeyAdvMainFragment.java | 115 ------------------ .../keychain/ui/ViewKeyAdvShareFragment.java | 38 ------ .../ui/dialog/ShareNfcDialogFragment.java | 97 --------------- .../res/layout/view_key_adv_main_fragment.xml | 88 -------------- .../layout/view_key_adv_share_fragment.xml | 45 ------- 5 files changed, 383 deletions(-) delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareNfcDialogFragment.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvMainFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvMainFragment.java index c9d20f9f4..fc107d794 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvMainFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvMainFragment.java @@ -37,15 +37,11 @@ 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.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.Log; import java.util.Date; @@ -55,24 +51,15 @@ public class ViewKeyAdvMainFragment extends LoaderFragment implements public static final String ARG_DATA_URI = "uri"; - private View mActionEdit; - private View mActionEditDivider; - private View mActionEncryptFiles; - private View mActionEncryptText; - private View mActionEncryptTextText; private View mActionCertify; private View mActionCertifyText; private ImageView mActionCertifyImage; - private View mActionUpdate; private ListView mUserIds; private static final int LOADER_ID_UNIFIED = 0; private static final int LOADER_ID_USER_IDS = 1; - // conservative attitude - private boolean mHasEncrypt = true; - private UserIdsAdapter mUserIdsAdapter; private Uri mDataUri; @@ -83,18 +70,12 @@ public class ViewKeyAdvMainFragment extends LoaderFragment implements View view = inflater.inflate(R.layout.view_key_adv_main_fragment, getContainer()); mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids); - 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); - mActionEncryptTextText = view.findViewById(R.id.view_key_action_encrypt_text_text); - mActionEncryptFiles = view.findViewById(R.id.view_key_action_encrypt_files); mActionCertify = view.findViewById(R.id.view_key_action_certify); mActionCertifyText = view.findViewById(R.id.view_key_action_certify_text); mActionCertifyImage = (ImageView) view.findViewById(R.id.view_key_action_certify_image); // make certify image gray, like action icons mActionCertifyImage.setColorFilter(getResources().getColor(R.color.tertiary_text_light), PorterDuff.Mode.SRC_IN); - mActionUpdate = view.findViewById(R.id.view_key_action_update); mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override @@ -139,37 +120,11 @@ public class ViewKeyAdvMainFragment extends LoaderFragment implements Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); - mActionEncryptFiles.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - encrypt(mDataUri, false); - } - }); - mActionEncryptText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - encrypt(mDataUri, true); - } - }); mActionCertify.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { certify(mDataUri); } }); - mActionEdit.setOnClickListener(new View.OnClickListener() { - public void onClick(View view) { - editKey(mDataUri); - } - }); - mActionUpdate.setOnClickListener(new View.OnClickListener() { - public void onClick(View view) { - try { - updateFromKeyserver(mDataUri, new ProviderHelper(getActivity())); - } catch (NotFoundException e) { - Notify.showNotify(getActivity(), R.string.error_key_not_found, Notify.Style.ERROR); - } - } - }); mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0); mUserIds.setAdapter(mUserIdsAdapter); @@ -222,45 +177,23 @@ public class ViewKeyAdvMainFragment extends LoaderFragment implements switch (loader.getId()) { case LOADER_ID_UNIFIED: { if (data.moveToFirst()) { - if (data.getInt(INDEX_UNIFIED_HAS_ANY_SECRET) != 0) { - // edit button - mActionEdit.setVisibility(View.VISIBLE); - mActionEditDivider.setVisibility(View.VISIBLE); - } else { - // edit button - mActionEdit.setVisibility(View.GONE); - mActionEditDivider.setVisibility(View.GONE); - } // If this key is revoked, it cannot be used for anything! if (data.getInt(INDEX_UNIFIED_IS_REVOKED) != 0) { - mActionEdit.setEnabled(false); mActionCertify.setEnabled(false); mActionCertifyText.setEnabled(false); - mActionEncryptText.setEnabled(false); - mActionEncryptTextText.setEnabled(false); - mActionEncryptFiles.setEnabled(false); } else { - mActionEdit.setEnabled(true); Date expiryDate = new Date(data.getLong(INDEX_UNIFIED_EXPIRY) * 1000); if (!data.isNull(INDEX_UNIFIED_EXPIRY) && expiryDate.before(new Date())) { mActionCertify.setEnabled(false); mActionCertifyText.setEnabled(false); - mActionEncryptText.setEnabled(false); - mActionEncryptTextText.setEnabled(false); - mActionEncryptFiles.setEnabled(false); } else { mActionCertify.setEnabled(true); mActionCertifyText.setEnabled(true); - mActionEncryptText.setEnabled(true); - mActionEncryptTextText.setEnabled(true); - mActionEncryptFiles.setEnabled(true); } } - mHasEncrypt = data.getInt(INDEX_UNIFIED_HAS_ENCRYPT) != 0; - break; } } @@ -286,48 +219,6 @@ public class ViewKeyAdvMainFragment extends LoaderFragment implements } } - private void encrypt(Uri dataUri, boolean text) { - // If there is no encryption key, don't bother. - if (!mHasEncrypt) { - Notify.showNotify(getActivity(), R.string.error_no_encrypt_subkey, Notify.Style.ERROR); - return; - } - try { - long keyId = new ProviderHelper(getActivity()) - .getCachedPublicKeyRing(dataUri) - .extractOrGetMasterKeyId(); - long[] encryptionKeyIds = new long[]{keyId}; - Intent intent; - if (text) { - intent = new Intent(getActivity(), EncryptTextActivity.class); - intent.setAction(EncryptTextActivity.ACTION_ENCRYPT_TEXT); - intent.putExtra(EncryptTextActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds); - } else { - intent = new Intent(getActivity(), EncryptFilesActivity.class); - intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA); - intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds); - } - // used instead of startActivity set actionbar based on callingPackage - startActivityForResult(intent, 0); - } catch (PgpKeyNotFoundException e) { - Log.e(Constants.TAG, "key not found!", e); - } - } - - private void updateFromKeyserver(Uri dataUri, ProviderHelper providerHelper) - throws ProviderHelper.NotFoundException { - byte[] blob = (byte[]) providerHelper.getGenericData( - KeychainContract.KeyRings.buildUnifiedKeyRingUri(dataUri), - KeychainContract.Keys.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB); - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(blob); - - Intent queryIntent = new Intent(getActivity(), ImportKeysActivity.class); - queryIntent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT); - queryIntent.putExtra(ImportKeysActivity.EXTRA_FINGERPRINT, fingerprint); - - startActivityForResult(queryIntent, 0); - } - private void certify(Uri dataUri) { long keyId = 0; try { @@ -342,10 +233,4 @@ public class ViewKeyAdvMainFragment extends LoaderFragment implements startActivityForResult(certifyIntent, 0); } - private void editKey(Uri dataUri) { - Intent editIntent = new Intent(getActivity(), EditKeyActivity.class); - editIntent.setData(KeychainContract.KeyRingData.buildSecretKeyRingUri(dataUri)); - startActivityForResult(editIntent, 0); - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java index 6208cff4e..6d019c5cd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java @@ -17,16 +17,13 @@ package org.sufficientlysecure.keychain.ui; -import android.annotation.TargetApi; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; -import android.provider.Settings; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; @@ -47,7 +44,6 @@ import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.ui.dialog.ShareNfcDialogFragment; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.QrCodeUtils; @@ -68,8 +64,6 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements private View mKeyShareButton; private View mKeyClipboardButton; private ImageButton mKeySafeSlingerButton; - private View mNfcHelpButton; - private View mNfcPrefsButton; private View mKeyUploadButton; ProviderHelper mProviderHelper; @@ -92,19 +86,11 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements mKeyShareButton = view.findViewById(R.id.view_key_action_key_share); mKeyClipboardButton = view.findViewById(R.id.view_key_action_key_clipboard); mKeySafeSlingerButton = (ImageButton) view.findViewById(R.id.view_key_action_key_safeslinger); - mNfcHelpButton = view.findViewById(R.id.view_key_action_nfc_help); - mNfcPrefsButton = view.findViewById(R.id.view_key_action_nfc_prefs); mKeyUploadButton = view.findViewById(R.id.view_key_action_upload); mKeySafeSlingerButton.setColorFilter(getResources().getColor(R.color.tertiary_text_light), PorterDuff.Mode.SRC_IN); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - mNfcPrefsButton.setVisibility(View.VISIBLE); - } else { - mNfcPrefsButton.setVisibility(View.GONE); - } - mFingerprintQrCode.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -142,18 +128,6 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements startSafeSlinger(mDataUri); } }); - mNfcHelpButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - showNfcHelpDialog(); - } - }); - mNfcPrefsButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - showNfcPrefs(); - } - }); mKeyUploadButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -243,18 +217,6 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements startActivity(qrCodeIntent); } - private void showNfcHelpDialog() { - ShareNfcDialogFragment dialog = ShareNfcDialogFragment.newInstance(); - dialog.show(getActivity().getSupportFragmentManager(), "shareNfcDialog"); - } - - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - private void showNfcPrefs() { - Intent intentSettings = new Intent( - Settings.ACTION_NFCSHARING_SETTINGS); - startActivity(intentSettings); - } - @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareNfcDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareNfcDialogFragment.java deleted file mode 100644 index 961f92f03..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareNfcDialogFragment.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2013-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.dialog; - -import android.annotation.TargetApi; -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.nfc.NfcAdapter; -import android.os.Build; -import android.os.Bundle; -import android.provider.Settings; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.FragmentActivity; - -import org.sufficientlysecure.htmltextview.HtmlTextView; -import org.sufficientlysecure.keychain.R; - -@TargetApi(Build.VERSION_CODES.JELLY_BEAN) -public class ShareNfcDialogFragment extends DialogFragment { - - /** - * Creates new instance of this fragment - */ - public static ShareNfcDialogFragment newInstance() { - ShareNfcDialogFragment frag = new ShareNfcDialogFragment(); - - return frag; - } - - /** - * Creates dialog - */ - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final FragmentActivity activity = getActivity(); - - CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(activity); - - alert.setTitle(R.string.share_nfc_dialog); - alert.setCancelable(true); - - alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - dismiss(); - } - }); - - HtmlTextView textView = new HtmlTextView(getActivity()); - textView.setPadding(8, 8, 8, 8); - alert.setView(textView); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { - textView.setText(getString(R.string.error) + ": " - + getString(R.string.error_jelly_bean_needed)); - } else { - // check if NFC Adapter is available - NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); - if (nfcAdapter == null) { - textView.setText(getString(R.string.error) + ": " - + getString(R.string.error_nfc_needed)); - } else { - // nfc works... - textView.setHtmlFromRawResource(getActivity(), R.raw.nfc_beam_share, true); - - alert.setNegativeButton(R.string.menu_beam_preferences, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - Intent intentSettings = new Intent( - Settings.ACTION_NFCSHARING_SETTINGS); - startActivity(intentSettings); - } - } - ); - } - } - - return alert.show(); - } -} diff --git a/OpenKeychain/src/main/res/layout/view_key_adv_main_fragment.xml b/OpenKeychain/src/main/res/layout/view_key_adv_main_fragment.xml index 6d0ed3298..d4272b816 100644 --- a/OpenKeychain/src/main/res/layout/view_key_adv_main_fragment.xml +++ b/OpenKeychain/src/main/res/layout/view_key_adv_main_fragment.xml @@ -71,94 +71,6 @@ android:layout_height="1dip" android:background="?android:attr/listDivider" /> - - - - - - - - - - - - - - - - - - - diff --git a/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml b/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml index cd8f96e6f..127fe36de 100644 --- a/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml +++ b/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml @@ -162,51 +162,6 @@ android:drawablePadding="8dp" android:gravity="center_vertical" /> - - - - - - - - - - - - From 11c34364545ff99268949e558cddcb853ea1810b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Fri, 27 Feb 2015 01:44:40 +0100 Subject: [PATCH 24/26] Prettify qr code in advanced key view --- .../keychain/ui/ViewKeyAdvShareFragment.java | 30 +++++++++++++---- .../layout/view_key_adv_share_fragment.xml | 32 ++++++++++++++++--- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java index 6d019c5cd..8d0a2dd1d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java @@ -17,16 +17,20 @@ package org.sufficientlysecure.keychain.ui; +import android.app.ActivityOptions; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; +import android.support.v4.app.ActivityCompat; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; +import android.support.v7.widget.CardView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -58,7 +62,8 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements public static final String ARG_DATA_URI = "uri"; private TextView mFingerprint; - private ImageView mFingerprintQrCode; + private ImageView mQrCode; + private CardView mQrCodeLayout; private View mFingerprintShareButton; private View mFingerprintClipboardButton; private View mKeyShareButton; @@ -80,7 +85,8 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements mProviderHelper = new ProviderHelper(ViewKeyAdvShareFragment.this.getActivity()); mFingerprint = (TextView) view.findViewById(R.id.view_key_fingerprint); - mFingerprintQrCode = (ImageView) view.findViewById(R.id.view_key_fingerprint_qr_code_image); + mQrCode = (ImageView) view.findViewById(R.id.view_key_qr_code); + mQrCodeLayout = (CardView) view.findViewById(R.id.view_key_qr_code_layout); mFingerprintShareButton = view.findViewById(R.id.view_key_action_fingerprint_share); mFingerprintClipboardButton = view.findViewById(R.id.view_key_action_fingerprint_clipboard); mKeyShareButton = view.findViewById(R.id.view_key_action_key_share); @@ -91,7 +97,7 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements mKeySafeSlingerButton.setColorFilter(getResources().getColor(R.color.tertiary_text_light), PorterDuff.Mode.SRC_IN); - mFingerprintQrCode.setOnClickListener(new View.OnClickListener() { + mQrCodeLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showQrCodeDialog(); @@ -213,8 +219,18 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements private void showQrCodeDialog() { Intent qrCodeIntent = new Intent(getActivity(), QrCodeViewActivity.class); + + // create the transition animation - the images in the layouts + // of both activities are defined with android:transitionName="qr_code" + Bundle opts = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ActivityOptions options = ActivityOptions + .makeSceneTransitionAnimation(getActivity(), mQrCodeLayout, "qr_code"); + opts = options.toBundle(); + } + qrCodeIntent.setData(mDataUri); - startActivity(qrCodeIntent); + ActivityCompat.startActivity(getActivity(), qrCodeIntent, opts); } @Override @@ -325,14 +341,14 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements // scale the image up to our actual size. we do this in code rather // than let the ImageView do this because we don't require filtering. Bitmap scaled = Bitmap.createScaledBitmap(qrCode, - mFingerprintQrCode.getHeight(), mFingerprintQrCode.getHeight(), + mQrCode.getHeight(), mQrCode.getHeight(), false); - mFingerprintQrCode.setImageBitmap(scaled); + mQrCode.setImageBitmap(scaled); // simple fade-in animation AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f); anim.setDuration(200); - mFingerprintQrCode.startAnimation(anim); + mQrCode.startAnimation(anim); } } }; diff --git a/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml b/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml index 127fe36de..1056a4681 100644 --- a/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml +++ b/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml @@ -1,4 +1,5 @@ @@ -67,15 +68,36 @@ android:layout_height="1dip" android:background="?android:attr/listDivider" /> - + android:clickable="true" + android:foreground="?android:attr/selectableItemBackground" + card_view:cardBackgroundColor="@android:color/white" + card_view:cardUseCompatPadding="true" + card_view:cardCornerRadius="4dp"> + + + + + + + + + + + + + Date: Fri, 27 Feb 2015 01:47:06 +0100 Subject: [PATCH 25/26] Prettify qr code cleanup --- .../main/res/layout/view_key_adv_share_fragment.xml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml b/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml index 1056a4681..7b382dca5 100644 --- a/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml +++ b/OpenKeychain/src/main/res/layout/view_key_adv_share_fragment.xml @@ -89,16 +89,6 @@ android:layout_height="match_parent" /> - - - - - - - - - - Date: Fri, 27 Feb 2015 18:05:36 +0100 Subject: [PATCH 26/26] go to directly to certify screen after nfc import --- OpenKeychain/src/main/AndroidManifest.xml | 18 ++-- .../keychain/ui/ImportKeysActivity.java | 63 -------------- ...vity.java => ImportKeysProxyActivity.java} | 84 +++++++++++++++---- .../keychain/ui/KeyListFragment.java | 4 +- .../keychain/ui/ViewKeyActivity.java | 4 +- 5 files changed, 79 insertions(+), 94 deletions(-) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/{QrCodeScanActivity.java => ImportKeysProxyActivity.java} (79%) diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 29b748410..20e618320 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -451,7 +451,7 @@ android:value=".ui.MainActivity" /> + + + + + + + + - - - - - - - - diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java index 6638c9944..71f6fd4bf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java @@ -17,17 +17,12 @@ package org.sufficientlysecure.keychain.ui; -import android.annotation.TargetApi; import android.app.ProgressDialog; import android.content.Intent; import android.net.Uri; -import android.nfc.NdefMessage; -import android.nfc.NfcAdapter; -import android.os.Build; import android.os.Bundle; import android.os.Message; import android.os.Messenger; -import android.os.Parcelable; import android.support.v4.app.Fragment; import android.view.View; import android.view.View.OnClickListener; @@ -63,9 +58,6 @@ public class ImportKeysActivity extends BaseActivity { // Actions for internal use only: public static final String ACTION_IMPORT_KEY_FROM_FILE = Constants.INTENT_PREFIX + "IMPORT_KEY_FROM_FILE"; - public static final String ACTION_IMPORT_KEY_FROM_NFC = Constants.INTENT_PREFIX - + "IMPORT_KEY_FROM_NFC"; - public static final String EXTRA_RESULT = "result"; // only used by ACTION_IMPORT_KEY @@ -215,15 +207,6 @@ public class ImportKeysActivity extends BaseActivity { startListFragment(savedInstanceState, null, null, null); break; } - case ACTION_IMPORT_KEY_FROM_NFC: { - // NOTE: this only displays the appropriate fragment, no actions are taken - startFileFragment(savedInstanceState); - // TODO!!!!! - - // no immediate actions! - startListFragment(savedInstanceState, null, null, null); - break; - } default: { startCloudFragment(savedInstanceState, null, false); startListFragment(savedInstanceState, null, null, null); @@ -433,50 +416,4 @@ public class ImportKeysActivity extends BaseActivity { } } - /** - * NFC - */ - @Override - public void onResume() { - super.onResume(); - - // Check to see if the Activity started due to an Android Beam - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) { - handleActionNdefDiscovered(getIntent()); - } else { - Log.d(Constants.TAG, "NFC: No NDEF discovered!"); - } - } else { - Log.e(Constants.TAG, "Android Beam not supported by Android < 4.1"); - } - } - - /** - * NFC - */ - @Override - public void onNewIntent(Intent intent) { - // onResume gets called after this to handle the intent - setIntent(intent); - } - - /** - * NFC: Parses the NDEF Message from the intent and prints to the TextView - */ - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - void handleActionNdefDiscovered(Intent intent) { - Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); - // only one message sent during the beam - NdefMessage msg = (NdefMessage) rawMsgs[0]; - // record 0 contains the MIME type, record 1 is the AAR, if present - byte[] receivedKeyringBytes = msg.getRecords()[0].getPayload(); - - Intent importIntent = new Intent(this, ImportKeysActivity.class); - importIntent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY); - importIntent.putExtra(ImportKeysActivity.EXTRA_KEY_BYTES, receivedKeyringBytes); - - handleActions(null, importIntent); - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeScanActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java similarity index 79% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeScanActivity.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java index 1a7a028c6..4cb6c69e0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeScanActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java @@ -17,12 +17,17 @@ package org.sufficientlysecure.keychain.ui; +import android.annotation.TargetApi; import android.app.ProgressDialog; import android.content.Intent; import android.net.Uri; +import android.nfc.NdefMessage; +import android.nfc.NfcAdapter; +import android.os.Build; import android.os.Bundle; import android.os.Message; import android.os.Messenger; +import android.os.Parcelable; import android.support.v4.app.FragmentActivity; import android.widget.Toast; @@ -48,7 +53,7 @@ import java.util.Locale; /** * Proxy activity (just a transparent content view) to scan QR Codes using the Barcode Scanner app */ -public class QrCodeScanActivity extends FragmentActivity { +public class ImportKeysProxyActivity extends FragmentActivity { public static final String ACTION_QR_CODE_API = OpenKeychainIntents.IMPORT_KEY_FROM_QR_CODE; public static final String ACTION_SCAN_WITH_RESULT = Constants.INTENT_PREFIX + "SCAN_QR_CODE_WITH_RESULT"; @@ -88,6 +93,15 @@ public class QrCodeScanActivity extends FragmentActivity { returnResult = false; new IntentIntegrator(this).initiateScan(); + } else if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) { + // Check to see if the Activity started due to an Android Beam + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + returnResult = false; + handleActionNdefDiscovered(getIntent()); + } else { + Log.e(Constants.TAG, "Android Beam not supported by Android < 4.1"); + finish(); + } } else { Log.e(Constants.TAG, "No valid scheme or action given!"); finish(); @@ -116,6 +130,7 @@ public class QrCodeScanActivity extends FragmentActivity { returnResult(data); } else { super.onActivityResult(requestCode, resultCode, data); + finish(); } } @@ -146,7 +161,28 @@ public class QrCodeScanActivity extends FragmentActivity { } } + public void importKeys(byte[] keyringData) { + + ParcelableKeyRing keyEntry = new ParcelableKeyRing(keyringData); + ArrayList selectedEntries = new ArrayList<>(); + selectedEntries.add(keyEntry); + + startImportService(selectedEntries); + + } + public void importKeys(String fingerprint) { + + ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null, null); + ArrayList selectedEntries = new ArrayList<>(); + selectedEntries.add(keyEntry); + + startImportService(selectedEntries); + + } + + private void startImportService (ArrayList keyRings) { + // Message is received after importing is done in KeychainIntentService KeychainIntentServiceHandler serviceHandler = new KeychainIntentServiceHandler( this, @@ -180,34 +216,32 @@ public class QrCodeScanActivity extends FragmentActivity { return; } - Intent certifyIntent = new Intent(QrCodeScanActivity.this, CertifyKeyActivity.class); + Intent certifyIntent = new Intent(ImportKeysProxyActivity.this, + CertifyKeyActivity.class); certifyIntent.putExtra(CertifyKeyActivity.EXTRA_RESULT, result); - certifyIntent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, result.getImportedMasterKeyIds()); + certifyIntent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, + result.getImportedMasterKeyIds()); startActivityForResult(certifyIntent, 0); } } }; - // search config - Preferences prefs = Preferences.getPreferences(this); - Preferences.CloudSearchPrefs cloudPrefs = new Preferences.CloudSearchPrefs(true, true, prefs.getPreferredKeyserver()); - - // Send all information needed to service to query keys in other thread - Intent intent = new Intent(this, KeychainIntentService.class); - - intent.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING); - // fill values for this action Bundle data = new Bundle(); - data.putString(KeychainIntentService.IMPORT_KEY_SERVER, cloudPrefs.keyserver); + // search config + { + Preferences prefs = Preferences.getPreferences(this); + Preferences.CloudSearchPrefs cloudPrefs = + new Preferences.CloudSearchPrefs(true, true, prefs.getPreferredKeyserver()); + data.putString(KeychainIntentService.IMPORT_KEY_SERVER, cloudPrefs.keyserver); + } - ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null, null); - ArrayList selectedEntries = new ArrayList<>(); - selectedEntries.add(keyEntry); - - data.putParcelableArrayList(KeychainIntentService.IMPORT_KEY_LIST, selectedEntries); + data.putParcelableArrayList(KeychainIntentService.IMPORT_KEY_LIST, keyRings); + // Send all information needed to service to query keys in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + intent.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING); intent.putExtra(KeychainIntentService.EXTRA_DATA, data); // Create a new Messenger for the communication back @@ -219,6 +253,20 @@ public class QrCodeScanActivity extends FragmentActivity { // start service with intent startService(intent); + + } + + /** + * NFC: Parses the NDEF Message from the intent and prints to the TextView + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + void handleActionNdefDiscovered(Intent intent) { + Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); + // only one message sent during the beam + NdefMessage msg = (NdefMessage) rawMsgs[0]; + // record 0 contains the MIME type, record 1 is the AAR, if present + byte[] receivedKeyringBytes = msg.getRecords()[0].getPayload(); + importKeys(receivedKeyringBytes); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java index 3da185dd2..99714b4a0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -602,8 +602,8 @@ public class KeyListFragment extends LoaderFragment } private void scanQrCode() { - Intent scanQrCode = new Intent(getActivity(), QrCodeScanActivity.class); - scanQrCode.setAction(QrCodeScanActivity.ACTION_SCAN_WITH_RESULT); + Intent scanQrCode = new Intent(getActivity(), ImportKeysProxyActivity.class); + scanQrCode.setAction(ImportKeysProxyActivity.ACTION_SCAN_WITH_RESULT); startActivityForResult(scanQrCode, 0); } 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 5c7a4448b..afb742079 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -316,8 +316,8 @@ public class ViewKeyActivity extends BaseActivity implements } private void scanQrCode() { - Intent scanQrCode = new Intent(this, QrCodeScanActivity.class); - scanQrCode.setAction(QrCodeScanActivity.ACTION_SCAN_WITH_RESULT); + Intent scanQrCode = new Intent(this, ImportKeysProxyActivity.class); + scanQrCode.setAction(ImportKeysProxyActivity.ACTION_SCAN_WITH_RESULT); startActivityForResult(scanQrCode, 0); }