diff options
Diffstat (limited to 'OpenKeychain')
10 files changed, 987 insertions, 11 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 ec26d4bbe..f79900aab 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 @@ -586,6 +586,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 (LogLevel.START, R.string.msg_se),          MSG_SE_INPUT_BYTES (LogLevel.INFO, R.string.msg_se_input_bytes), 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 2ee923e42..2ba0b6231 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; @@ -46,6 +47,10 @@ import org.spongycastle.openpgp.operator.jcajce.NfcSyncPublicKeyDataDecryptorFac  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;  import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;  import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; @@ -83,6 +88,8 @@ public class PgpDecryptVerify extends BaseOperation {      private boolean mDecryptMetadataOnly;      private byte[] mDecryptedSessionKey;      private byte[] mDetachedSignature; +    private String mRequiredSignerFingerprint; +    private boolean mSignedLiteralData;      protected PgpDecryptVerify(Builder builder) {          super(builder.mContext, builder.mProviderHelper, builder.mProgressable); @@ -97,6 +104,8 @@ public class PgpDecryptVerify extends BaseOperation {          this.mDecryptMetadataOnly = builder.mDecryptMetadataOnly;          this.mDecryptedSessionKey = builder.mDecryptedSessionKey;          this.mDetachedSignature = builder.mDetachedSignature; +        this.mSignedLiteralData = builder.mSignedLiteralData; +        this.mRequiredSignerFingerprint = builder.mRequiredSignerFingerprint;      }      public static class Builder { @@ -114,6 +123,8 @@ public class PgpDecryptVerify extends BaseOperation {          private boolean mDecryptMetadataOnly = false;          private byte[] mDecryptedSessionKey = null;          private byte[] mDetachedSignature = null; +        private String mRequiredSignerFingerprint = null; +        private boolean mSignedLiteralData = false;          public Builder(Context context, ProviderHelper providerHelper,                         Progressable progressable, @@ -125,6 +136,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; @@ -189,7 +218,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);                      } else { @@ -214,6 +245,136 @@ 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); + +        JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in); +        Object o = pgpF.nextObject(); +        if (o instanceof PGPCompressedData) { +            log.add(LogType.MSG_DC_CLEAR_DECOMPRESS, indent + 1); + +            pgpF = new JcaPGPObjectFactory(((PGPCompressedData) o).getDataStream()); +            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); +        } + +        String fingerprint = KeyFormattingUtils.convertFingerprintToHex(signingRing.getFingerprint()); +        if (!(mRequiredSignerFingerprint.equals(fingerprint))) { +            log.add(LogType.MSG_VL_ERROR_MISSING_KEY, indent); +            Log.d(Constants.TAG, "Fingerprint mismatch; wanted " + mRequiredSignerFingerprint + +                    " got " + fingerprint + "!"); +            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); + +        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); +        } + +        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(signatureResult); +        return result; +    } + + +    /**       * Decrypt and/or verifies binary or ascii armored pgp       */      private DecryptVerifyResult decryptVerify(InputStream in, int indent) throws IOException, PGPException { @@ -672,6 +833,7 @@ 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! +            Log.d(Constants.TAG, "MDC fail");              if (!signatureResultBuilder.isValidSignature()) {                  log.add(LogType.MSG_DC_ERROR_INTEGRITY_MISSING, 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 72eec6955..d95701458 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -26,7 +26,13 @@ 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; @@ -44,6 +50,7 @@ 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.OperationLog;  import org.sufficientlysecure.keychain.operations.results.PromoteKeyResult;  import org.sufficientlysecure.keychain.operations.results.SignEncryptResult;  import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; @@ -62,10 +69,19 @@ 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.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; +import de.measite.minidns.record.TXT; +  /**   * This Service contains all important long lasting operations for OpenKeychain. It receives Intents with   * data from the activities or other apps, queues these intents, executes them, and stops itself @@ -82,6 +98,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"; @@ -106,6 +124,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; @@ -120,6 +139,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"; @@ -240,7 +263,7 @@ public class KeychainIntentService extends IntentService implements Progressable                  break;              } -            case ACTION_DECRYPT_METADATA: +            case ACTION_DECRYPT_METADATA: {                  try {                  /* Input */ @@ -271,7 +294,106 @@ public class KeychainIntentService extends IntentService implements Progressable                  }                  break; -            case ACTION_DECRYPT_VERIFY: +            } +            case ACTION_VERIFY_KEYBASE_PROOF: { + +                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 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) { +                        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<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); + +                    PgpDecryptVerify.Builder builder = new PgpDecryptVerify.Builder( +                            this, new ProviderHelper(this), this, +                            inputData, outStream +                    ); +                    builder.setSignedLiteralData(true).setRequiredSignerFingerprint(requiredFingerprint); + +                    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); +                } + +                break; +            } +            case ACTION_DECRYPT_VERIFY: {                  try {                  /* Input */ @@ -313,6 +435,7 @@ public class KeychainIntentService extends IntentService implements Progressable                  }                  break; +            }              case ACTION_DELETE: {                  // Input @@ -403,7 +526,7 @@ public class KeychainIntentService extends IntentService implements Progressable                  break;              } -            case ACTION_SIGN_ENCRYPT: +            case ACTION_SIGN_ENCRYPT: {                  // Input                  SignEncryptParcel inputParcel = data.getParcelable(SIGN_ENCRYPT_PARCEL); @@ -417,16 +540,15 @@ public class KeychainIntentService extends IntentService implements Progressable                  sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, result);                  break; - -            case ACTION_UPLOAD_KEYRING: - +            } +            case ACTION_UPLOAD_KEYRING: {                  try { -                /* Input */ +                    /* Input */                      String keyServer = data.getString(UPLOAD_KEY_SERVER);                      // and dataUri! -                /* Operation */ +                    /* Operation */                      HkpKeyserver server = new HkpKeyserver(keyServer);                      CanonicalizedPublicKeyRing keyring = providerHelper.getCanonicalizedPublicKeyRing(dataUri); @@ -443,8 +565,24 @@ public class KeychainIntentService extends IntentService implements Progressable                      sendErrorToHandler(e);                  }                  break; +            }          } +    } +    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) { @@ -457,7 +595,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 6aae1269f..ceb0a2d2b 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/ViewKeyAdvActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java index 37f9113bb..894529cc5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java @@ -144,6 +144,11 @@ public class ViewKeyAdvActivity extends BaseActivity implements          mTabsAdapter.addTab(ViewKeyAdvCertsFragment.class,                  certsBundle, getString(R.string.key_view_tab_certs)); +        Bundle trustBundle = new Bundle(); +        trustBundle.putParcelable(ViewKeyTrustFragment.ARG_DATA_URI, dataUri); +        mTabsAdapter.addTab(ViewKeyTrustFragment.class, +                trustBundle, getString(R.string.key_view_tab_keybase)); +          // 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..5483d16b8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java @@ -0,0 +1,460 @@ +/* + * 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> { + +    public static final String ARG_DATA_URI = "uri"; + +    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(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) { +                        mStartSearch.setEnabled(false); +                        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(); +                String verify = getString(R.string.keybase_verify); +                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_verifying_signature), 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); +    } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java index 598793233..6ba9e26ad 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java @@ -213,8 +213,24 @@ public class ImportKeysAdapter extends ArrayAdapter<ImportKeysListEntry> {              // destroyLoader view from holder              holder.userIdsList.removeAllViews(); +            // we want conventional gpg UserIDs first, then Keybase ”proofs”              HashMap<String, HashSet<String>> mergedUserIds = entry.getMergedUserIds(); -            for (Map.Entry<String, HashSet<String>> pair : mergedUserIds.entrySet()) { +            ArrayList<Map.Entry<String, HashSet<String>>> sortedIds = new ArrayList<Map.Entry<String, HashSet<String>>>(mergedUserIds.entrySet()); +            java.util.Collections.sort(sortedIds, new java.util.Comparator<Map.Entry<String, HashSet<String>>>() { +                @Override +                public int compare(Map.Entry<String, HashSet<String>> entry1, Map.Entry<String, HashSet<String>> entry2) { + +                    // sort keybase UserIds after non-Keybase +                    boolean e1IsKeybase = entry1.getKey().contains(":"); +                    boolean e2IsKeybase = entry2.getKey().contains(":"); +                    if (e1IsKeybase != e2IsKeybase) { +                        return (e1IsKeybase) ? 1 : -1; +                    } +                    return entry1.getKey().compareTo(entry2.getKey()); +                } +            }); + +            for (Map.Entry<String, HashSet<String>> pair : sortedIds) {                  String cUserId = pair.getKey();                  HashSet<String> cEmails = pair.getValue(); 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<TableRow xmlns:android="http://schemas.android.com/apk/res/android" +    android:layout_width="match_parent" android:layout_height="match_parent"> +    <TextView +        android:id="@+id/proof_number" +        android:layout_width="wrap_content" +        android:layout_height="wrap_content" +        android:paddingLeft="6dip" +        android:text="1." +        style="?android:attr/textAppearanceMedium" /> + +    <TextView +        android:id="@+id/proof_text" +        android:layout_width="wrap_content" +        android:layout_height="wrap_content" +        android:paddingLeft="6dip" +        android:text="Posts to twitter as Timbray" +        style="?android:attr/textAppearanceMedium" /> +</TableRow> 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 @@ +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" +    android:layout_width="match_parent" +    android:layout_height="match_parent"> + +    <!-- focusable and related properties to workaround http://stackoverflow.com/q/16182331--> +    <LinearLayout +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:focusable="true" +        android:focusableInTouchMode="true" +        android:descendantFocusability="beforeDescendants" +        android:orientation="vertical" +        android:paddingLeft="16dp" +        android:paddingRight="16dp"> + +        <TextView +            style="@style/SectionHeader" +            android:layout_width="wrap_content" +            android:layout_height="0dp" +            android:layout_marginTop="8dp" +            android:text="@string/section_should_you_trust" +            android:layout_weight="1" /> + +        <TextView +            android:id="@+id/view_key_trust_readout" +            android:layout_width="wrap_content" +            android:layout_height="0dp" +            android:layout_marginTop="14dp" +            android:layout_marginLeft="8dp" +            android:layout_weight="1" +            style="?android:attr/textAppearanceMedium"/> + +        <TextView +            style="@style/SectionHeader" +            android:layout_width="wrap_content" +            android:layout_height="0dp" +            android:layout_marginTop="14dp" +            android:text="@string/section_cloud_evidence" +            android:layout_weight="1" /> + +        <LinearLayout +            android:id="@+id/view_key_trust_search_cloud" +            android:layout_width="match_parent" +            android:layout_height="?android:attr/listPreferredItemHeight" +            android:clickable="true" +            android:paddingRight="4dp" +            style="@style/SelectableItem" +            android:orientation="horizontal"> + +            <TextView +                android:paddingLeft="8dp" +                android:textAppearance="?android:attr/textAppearanceMedium" +                android:layout_width="0dip" +                android:layout_height="match_parent" +                android:text="@string/key_trust_start_cloud_search" +                android:layout_weight="1" +                android:gravity="center_vertical" /> + +            <ImageView +                android:layout_width="wrap_content" +                android:layout_height="match_parent" +                android:padding="8dp" +                android:src="@drawable/ic_action_search_cloud" +                android:layout_gravity="center_vertical" /> + +        </LinearLayout> + + +        <TextView +            android:id="@+id/view_key_trust_cloud_narrative" +            android:layout_width="wrap_content" +            android:layout_height="0dp" +            android:layout_marginTop="14dp" +            android:layout_marginBottom="14dp" +            android:layout_marginLeft="8dp" +            android:layout_weight="1" +            style="?android:attr/textAppearanceMedium" /> + +        <TableLayout +            android:id="@+id/view_key_proof_list" +            android:layout_width="match_parent" +            android:layout_height="wrap_content"/> + +        <TextView +            android:id="@+id/view_key_proof_verify_header" +            style="@style/SectionHeader" +            android:layout_width="wrap_content" +            android:layout_height="0dp" +            android:layout_marginTop="16dp" +            android:text="@string/section_proof_details" +            android:layout_weight="1" /> + +        <TextView +            android:id="@+id/view_key_proof_verify_detail" +            android:layout_width="wrap_content" +            android:layout_height="0dp" +            android:layout_marginTop="14dp" +            android:layout_marginLeft="8dp" +            android:layout_weight="1" +            style="?android:attr/textAppearanceMedium"/> + + +    </LinearLayout> + +</ScrollView> diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index e4b7cdd20..f43b4e623 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -45,6 +45,9 @@      <!-- section -->      <string name="section_user_ids">"Identities"</string> +    <string name="section_should_you_trust">"Should you trust this key?"</string> +    <string name="section_proof_details">Proof verification</string> +    <string name="section_cloud_evidence">"Evidence from the cloud"</string>      <string name="section_keys">"Subkeys"</string>      <string name="section_cloud_search">"Cloud search"</string>      <string name="section_general">"General"</string> @@ -541,6 +544,7 @@      <string name="key_view_tab_share">"Share"</string>      <string name="key_view_tab_keys">"Subkeys"</string>      <string name="key_view_tab_certs">"Certificates"</string> +    <string name="key_view_tab_keybase">"Keybase.io"</string>      <string name="user_id_info_revoked_title">"Revoked"</string>      <string name="user_id_info_revoked_text">"This identity has been revoked by the key owner. It is no longer valid."</string>      <string name="user_id_info_certified_title">"Certified"</string> @@ -550,6 +554,47 @@      <string name="user_id_info_invalid_title">"Invalid"</string>      <string name="user_id_info_invalid_text">"Something is wrong with this identity!"</string> +    <!-- Key trust --> +    <string name="key_trust_already_verified">"You have already certified this key!"</string> +    <string name="key_trust_it_is_yours">"This is one of your keys!"</string> +    <string name="key_trust_maybe">"This key is neither revoked nor expired.\nYou haven’t certified it, but you may choose to trust it."</string> +    <string name="key_trust_revoked">"This key has been revoked by its owner. You should not trust it."</string> +    <string name="key_trust_expired">"This key has expired. You should not trust it."</string> +    <string name="key_trust_old_keys">" It may be OK to use this to decrypt an old message dating from the time when this key was valid."</string> +    <string name="key_trust_no_cloud_evidence">"No evidence from the cloud on this key’s trustworthiness."</string> +    <string name="key_trust_start_cloud_search">"Start search"</string> +    <string name="key_trust_results_prefix">"Keybase.io offers “proofs” which assert that the owner of this key: "</string> + +    <!-- keybase proof stuff --> +    <string name="keybase_narrative_twitter">"Posts to Twitter as"</string> +    <string name="keybase_narrative_github">"Is known on GitHub as"</string> +    <string name="keybase_narrative_dns">"Controls the domain name(s)"</string> +    <string name="keybase_narrative_web_site">"Can post to the Web site(s)"</string> +    <string name="keybase_narrative_reddit">"Posts to Reddit as"</string> +    <string name="keybase_narrative_coinbase">"Is known on Coinbase as"</string> +    <string name="keybase_narrative_hackernews">"Posts to Hacker News as"</string> +    <string name="keybase_narrative_unknown">"Unknown proof type"</string> +    <string name="keybase_proof_failure">"Unfortunately this proof cannot be verified."</string> +    <string name="keybase_unknown_proof_failure">"Unrecognized problem with proof checker"</string> +    <string name="keybase_problem_fetching_evidence">"Problem with proof evidence"</string> +    <string name="keybase_key_mismatch">"Key fingerprint doesn’t match that in proof post"</string> +    <string name="keybase_dns_query_failure">"DNS TXT Record retrieval failed"</string> +    <string name="keybase_no_prover_found">"No proof checker found for"</string> +    <string name="keybase_message_payload_mismatch">"Decrypted proof post does not match expected value"</string> +    <string name="keybase_message_fetching_data">"Fetching proof evidence"</string> +    <string name="keybase_proof_succeeded">"This proof has been verified!"</string> +    <string name="keybase_a_post">"A post"</string> +    <string name="keybase_fetched_from">"fetched from"</string> +    <string name="keybase_for_the_domain">"for the domain"</string> +    <string name="keybase_contained_signature">"contains a message which could only have been created by the owner of this key."</string> +    <string name="keybase_twitter_proof">"A tweet"</string> +    <string name="keybase_dns_proof">"A DNS TXT record"</string> +    <string name="keybase_web_site_proof">"A text file"</string> +    <string name="keybase_github_proof">"A gist"</string> +    <string name="keybase_reddit_proof">"A JSON file"</string> +    <string name="keybase_reddit_attribution">"attributed by Reddit to"</string> +    <string name="keybase_verify">"Verify"</string> +      <!-- Edit key -->      <string name="edit_key_action_change_passphrase">"Change Passphrase"</string>      <string name="edit_key_action_add_identity">"Add Identity"</string> @@ -974,6 +1019,19 @@      <string name="msg_dc_trail_unknown">"Encountered trailing data of unknown type"</string>      <string name="msg_dc_unlocking">"Unlocking secret key"</string> +    <!-- Messages for VerifySignedLiteralData operation --> +    <string name="msg_vl">"Starting signature check"</string> +    <string name="msg_vl_error_no_siglist">"No signature list in signed literal data"</string> +    <string name="msg_vl_error_wrong_key">"Message not signed with right key"</string> +    <string name="msg_vl_error_missing_literal">"No payload in signed literal data"</string> +    <string name="msg_vl_clear_meta_file">"Filename: %s"</string> +    <string name="msg_vl_clear_meta_mime">"MIME type: %s"</string> +    <string name="msg_vl_clear_meta_time">"Modification time: %s"</string> +    <string name="msg_vl_clear_meta_size">"Filesize: %s"</string> +    <string name="msg_vl_clear_signature_check">"Verifying signature data"</string> +    <string name="msg_vl_error_integrity_check">"Integrity check error!"</string> +    <string name="msg_vl_ok">"OK"</string> +      <!-- Messages for SignEncrypt operation -->      <string name="msg_se">"Starting sign/encrypt operation"</string>      <string name="msg_se_input_bytes">"Processing input from byte array"</string> | 
