aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src/main/java/org/sufficientlysecure
diff options
context:
space:
mode:
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure')
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java9
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java162
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java161
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java6
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java456
6 files changed, 778 insertions, 21 deletions
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<Long> 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<Long> 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);
}
@@ -196,6 +223,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
*/
private DecryptVerifyResult decryptVerify(InputStream in, int indent) throws IOException, PGPException {
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..8a670df25 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java
@@ -26,57 +26,70 @@ 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.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.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.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;
+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
@@ -93,6 +106,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";
@@ -115,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;
@@ -142,6 +158,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 +311,92 @@ 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;
+ }
+
+ String domain = prover.dnsTxtCheckRequired();
+ if (domain != null) {
+ Record[] records = new Client().query(new Question(domain, Record.TYPE.TXT)).getAnswers();
+ List<List<byte[]>> extents = new ArrayList<List<byte[]>>();
+ 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));
+ if (!prover.checkRawMessageBytes(messageByteStream)) {
+ sendProofError(prover.getLog(), null);
+ 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");
+
+ // 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);
+ }
+
} else if (ACTION_DECRYPT_VERIFY.equals(action)) {
try {
@@ -597,6 +703,22 @@ public class KeychainIntentService extends IntentService implements Progressable
}
+ private void sendProofError(List<String> log, String label) {
+ String msg = null;
+ label = (label == null) ? "" : label + ": ";
+ 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 +729,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/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/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..4965b2525
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2014 Tim Bray <tbray@textuality.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<Cursor> {
+
+ 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<Cursor> 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<Cursor> 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<Cursor> loader) {
+ // no-op in this case I think
+ }
+
+ class ResultPage {
+ String mHeader;
+ final List<CharSequence> mProofs;
+
+ public ResultPage(String header, List<CharSequence> proofs) {
+ mHeader = header;
+ mProofs = proofs;
+ }
+ }
+
+ // look for evidence from keybase in the background, make tabular version of result
+ //
+ private class DescribeKey extends AsyncTask<String, Void, ResultPage> {
+
+ @Override
+ protected ResultPage doInBackground(String... args) {
+ String fingerprint = args[0];
+
+ final ArrayList<CharSequence> proofList = new ArrayList<CharSequence>();
+ final Hashtable<Integer, ArrayList<Proof>> proofs = new Hashtable<Integer, ArrayList<Proof>>();
+ 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.mProofs.isEmpty()) {
+ 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<Integer, ArrayList<Proof>> table, Integer proofType, Proof proof) throws KeybaseException {
+ ArrayList<Proof> list = table.get(proofType);
+ if (list == null) {
+ list = new ArrayList<Proof>();
+ table.put(proofType, list);
+ }
+ list.add(proof);
+ }
+
+ // 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 true;
+ case Proof.PROOF_TYPE_WEB_SITE: return true;
+ case Proof.PROOF_TYPE_HACKERNEWS: return true;
+ case Proof.PROOF_TYPE_COINBASE: return true;
+ case Proof.PROOF_TYPE_REDDIT: return true;
+ 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 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();
+ 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();
+ 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);
+ }
+ mProofVerifyHeader.setVisibility(View.VISIBLE);
+ mProofVerifyDetail.setVisibility(View.VISIBLE);
+ mProofVerifyDetail.setMovementMethod(LinkMovementMethod.getInstance());
+ 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);
+ }
+}