diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java index 0ebcb321d..65f36cc70 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java @@ -18,11 +18,17 @@ package org.sufficientlysecure.keychain.operations.results; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import android.os.Parcel; import org.openintents.openpgp.OpenPgpDecryptionResult; import org.openintents.openpgp.OpenPgpMetadata; import org.openintents.openpgp.OpenPgpSignatureResult; +import org.sufficientlysecure.keychain.pgp.SecurityProblem; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; @@ -34,6 +40,7 @@ public class DecryptVerifyResult extends InputPendingResult { OpenPgpSignatureResult mSignatureResult; OpenPgpDecryptionResult mDecryptionResult; OpenPgpMetadata mDecryptionMetadata; + ArrayList mSecurityProblems; CryptoInputParcel mCachedCryptoInputParcel; @@ -65,6 +72,8 @@ public class DecryptVerifyResult extends InputPendingResult { mDecryptionMetadata = source.readParcelable(OpenPgpMetadata.class.getClassLoader()); mCachedCryptoInputParcel = source.readParcelable(CryptoInputParcel.class.getClassLoader()); mSkippedDisallowedKeys = source.createLongArray(); + + mSecurityProblems = (ArrayList) source.readSerializable(); } @@ -127,6 +136,8 @@ public class DecryptVerifyResult extends InputPendingResult { dest.writeParcelable(mDecryptionMetadata, flags); dest.writeParcelable(mCachedCryptoInputParcel, flags); dest.writeLongArray(mSkippedDisallowedKeys); + + dest.writeSerializable(mSecurityProblems); } public static final Creator CREATOR = new Creator() { @@ -139,4 +150,28 @@ public class DecryptVerifyResult extends InputPendingResult { } }; + public void addSecurityProblem(SecurityProblem securityProblem) { + if (securityProblem == null) { + return; + } + if (mSecurityProblems == null) { + mSecurityProblems = new ArrayList<>(); + } + mSecurityProblems.add(securityProblem); + } + + public void addSecurityProblems(List securityProblems) { + if (securityProblems == null) { + return; + } + if (mSecurityProblems == null) { + mSecurityProblems = new ArrayList<>(); + } + mSecurityProblems.addAll(securityProblems); + } + + public List getSecurityProblems() { + return mSecurityProblems != null ? + Collections.unmodifiableList(mSecurityProblems) : Collections.emptyList(); + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKey.java index d95cf9a31..fc0baaf07 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKey.java @@ -18,6 +18,15 @@ package org.sufficientlysecure.keychain.pgp; + +import java.io.IOException; +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Iterator; + import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; import org.bouncycastle.bcpg.ECDHPublicBCPGKey; @@ -36,14 +45,6 @@ import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.util.IterableIterator; import org.sufficientlysecure.keychain.util.Log; -import java.io.IOException; -import java.security.PublicKey; -import java.security.interfaces.ECPublicKey; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.Iterator; - /** Wrapper for a PGPPublicKey. * * The methods implemented in this class are a thin layer over @@ -141,7 +142,7 @@ public class CanonicalizedPublicKey extends UncachedPublicKey { } public boolean isSecure() { - return PgpSecurityConstants.isSecureKey(this); + return PgpSecurityConstants.checkForSecurityProblems(this) == null; } public long getValidSeconds() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java index 921aed262..75b612066 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java @@ -121,6 +121,29 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey { return this != UNAVAILABLE && this != GNU_DUMMY; } + /** Compares by "usability", which basically compares how independently usable + * two SecretKeyTypes are. The order is roughly this: + * + * empty passphrase < passphrase/others < divert < stripped + * + */ + public int compareUsability(SecretKeyType other) { + // if one is usable but the other isn't, the usable one comes first + if (isUsable() ^ other.isUsable()) { + return isUsable() ? -1 : 1; + } + // if one is a divert-to-card but the other isn't, the non-divert one comes first + if ((this == DIVERT_TO_CARD) ^ (other == DIVERT_TO_CARD)) { + return this != DIVERT_TO_CARD ? -1 : 1; + } + // if one requires a passphrase but another doesn't, the one without a passphrase comes first + if ((this == PASSPHRASE_EMPTY) ^ (other == PASSPHRASE_EMPTY)) { + return this == PASSPHRASE_EMPTY ? -1 : 1; + } + // all other (current) cases are equal + return 0; + } + } /** This method returns the SecretKeyType for this secret key, testing for an empty diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpDecryptionResultBuilder.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpDecryptionResultBuilder.java index 31a3f91b6..e2c0b6b63 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpDecryptionResultBuilder.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpDecryptionResultBuilder.java @@ -17,35 +17,49 @@ package org.sufficientlysecure.keychain.pgp; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import org.openintents.openpgp.OpenPgpDecryptionResult; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.pgp.SecurityProblem.SymmetricAlgorithmProblem; import org.sufficientlysecure.keychain.util.Log; public class OpenPgpDecryptionResultBuilder { // builder - private boolean mInsecure = false; - private boolean mEncrypted = false; + private boolean isEncrypted = false; private byte[] sessionKey; private byte[] decryptedSessionKey; + private ArrayList securityProblems; - public void setInsecure(boolean insecure) { - this.mInsecure = insecure; + public void addSecurityProblem(SecurityProblem securityProblem) { + if (securityProblems == null) { + securityProblems = new ArrayList<>(); + } + securityProblems.add(securityProblem); + } + + public List getKeySecurityProblems() { + return securityProblems != null ? Collections.unmodifiableList(securityProblems) : null; } public void setEncrypted(boolean encrypted) { - this.mEncrypted = encrypted; + this.isEncrypted = encrypted; } public OpenPgpDecryptionResult build() { - if (mInsecure) { + if (securityProblems != null && !securityProblems.isEmpty()) { Log.d(Constants.TAG, "RESULT_INSECURE"); return new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_INSECURE, sessionKey, decryptedSessionKey); } - if (mEncrypted) { + if (isEncrypted) { Log.d(Constants.TAG, "RESULT_ENCRYPTED"); - return new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_ENCRYPTED, sessionKey, decryptedSessionKey); + return new OpenPgpDecryptionResult( + OpenPgpDecryptionResult.RESULT_ENCRYPTED, sessionKey, decryptedSessionKey); } Log.d(Constants.TAG, "RESULT_NOT_ENCRYPTED"); @@ -60,4 +74,5 @@ public class OpenPgpDecryptionResultBuilder { this.sessionKey = sessionKey; this.decryptedSessionKey = decryptedSessionKey; } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java index 9c04c5394..83b99776f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java @@ -19,7 +19,9 @@ package org.sufficientlysecure.keychain.pgp; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; +import java.util.List; import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.OpenPgpSignatureResult.SenderStatusResult; @@ -53,9 +55,9 @@ public class OpenPgpSignatureResultBuilder { private boolean mIsSignatureKeyCertified = false; private boolean mIsKeyRevoked = false; private boolean mIsKeyExpired = false; - private boolean mInsecure = false; private String mSenderAddress; private Date mSignatureTimestamp; + private ArrayList mSecurityProblems; public OpenPgpSignatureResultBuilder(KeyRepository keyRepository) { this.mKeyRepository = keyRepository; @@ -81,8 +83,15 @@ public class OpenPgpSignatureResultBuilder { this.mValidSignature = validSignature; } - public void setInsecure(boolean insecure) { - this.mInsecure = insecure; + public void addSecurityProblem(SecurityProblem securityProblem) { + if (mSecurityProblems == null) { + mSecurityProblems = new ArrayList<>(); + } + mSecurityProblems.add(securityProblem); + } + + public List getSecurityProblems() { + return mSecurityProblems != null ? Collections.unmodifiableList(mSecurityProblems) : null; } public void setSignatureKeyCertified(boolean isSignatureKeyCertified) { @@ -106,10 +115,6 @@ public class OpenPgpSignatureResultBuilder { this.mConfirmedUserIds = confirmedUserIds; } - public boolean isInsecure() { - return mInsecure; - } - public void initValid(CanonicalizedPublicKey signingKey) { setSignatureAvailable(true); setKnownKey(true); @@ -184,7 +189,7 @@ public class OpenPgpSignatureResultBuilder { } else if (mIsKeyExpired) { Log.d(Constants.TAG, "RESULT_INVALID_KEY_EXPIRED"); signatureStatus = OpenPgpSignatureResult.RESULT_INVALID_KEY_EXPIRED; - } else if (mInsecure) { + } else if (mSecurityProblems != null && !mSecurityProblems.isEmpty()) { Log.d(Constants.TAG, "RESULT_INVALID_INSECURE"); signatureStatus = OpenPgpSignatureResult.RESULT_INVALID_KEY_INSECURE; } else if (mIsSignatureKeyCertified) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java index 743295007..d728b185c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java @@ -68,6 +68,9 @@ import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; +import org.sufficientlysecure.keychain.pgp.SecurityProblem.KeySecurityProblem; +import org.sufficientlysecure.keychain.pgp.SecurityProblem.MissingMdc; +import org.sufficientlysecure.keychain.pgp.SecurityProblem.SymmetricAlgorithmProblem; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; @@ -211,7 +214,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation skippedDisallowedEncryptionKeys = new HashSet<>(); - boolean insecureEncryptionKey = false; + KeySecurityProblem encryptionKeySecurityProblem = null; // convenience method to return with error public EncryptStreamResult with(DecryptVerifyResult result) { @@ -317,15 +320,17 @@ public class PgpDecryptVerifyOperation extends BaseOperation= 2048); + if (bitStrength < 2048) { + return new InsecureBitStrength(masterKeyId, subKeyId, algorithm, bitStrength); + } + return null; } // RSA_ENCRYPT, RSA_SIGN: deprecated in RFC 4880, use RSA_GENERAL with key flags case PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT: { - return (key.getBitStrength() >= 2048); + if (bitStrength < 2048) { + return new InsecureBitStrength(masterKeyId, subKeyId, algorithm, bitStrength); + } + return null; } case PublicKeyAlgorithmTags.DSA: { - return (key.getBitStrength() >= 2048); + if (bitStrength < 2048) { + return new InsecureBitStrength(masterKeyId, subKeyId, algorithm, bitStrength); + } + return null; } case PublicKeyAlgorithmTags.ECDH: case PublicKeyAlgorithmTags.ECDSA: { - return PgpSecurityConstants.sCurveWhitelist.contains(key.getCurveOid()); + if (!PgpSecurityConstants.sCurveWhitelist.contains(curveOid)) { + return new NotWhitelistedCurve(masterKeyId, subKeyId, curveOid, algorithm); + } + return null; } // ELGAMAL_GENERAL: deprecated in RFC 4880, use ELGAMAL_ENCRYPT // DIFFIE_HELLMAN: unsure + // TODO specialize all cases! default: - return false; + return new UnidentifiedKeyProblem(masterKeyId, subKeyId, algorithm); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignatureChecker.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignatureChecker.java index 2f83e9518..679dcc5d4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignatureChecker.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignatureChecker.java @@ -25,18 +25,21 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.security.SignatureException; +import java.util.List; -import org.openintents.openpgp.OpenPgpSignatureResult; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPOnePassSignature; import org.bouncycastle.openpgp.PGPOnePassSignatureList; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; +import org.openintents.openpgp.OpenPgpSignatureResult; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.provider.KeyWritableRepository; +import org.sufficientlysecure.keychain.pgp.SecurityProblem.InsecureHashAlgorithm; +import org.sufficientlysecure.keychain.pgp.SecurityProblem.KeySecurityProblem; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeyRepository; import org.sufficientlysecure.keychain.util.Log; @@ -134,10 +137,12 @@ class PgpSignatureChecker { } private void checkKeySecurity(OperationLog log, int indent) { - // TODO: checks on signingRing ? - if (!PgpSecurityConstants.isSecureKey(signingKey)) { + // TODO check primary key as well, not only the signing key + KeySecurityProblem keySecurityProblem = + PgpSecurityConstants.checkForSecurityProblems(signingKey); + if (keySecurityProblem != null) { log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1); - signatureResultBuilder.setInsecure(true); + signatureResultBuilder.addSecurityProblem(keySecurityProblem); } } @@ -233,9 +238,11 @@ class PgpSignatureChecker { } // check for insecure hash algorithms - if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) { + InsecureHashAlgorithm signatureSecurityProblem = + PgpSecurityConstants.checkSignatureAlgorithmForSecurityProblems(signature.getHashAlgorithm()); + if (signatureSecurityProblem != null) { log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1); - signatureResultBuilder.setInsecure(true); + signatureResultBuilder.addSecurityProblem(signatureSecurityProblem); } signatureResultBuilder.setSignatureTimestamp(signature.getCreationTime()); @@ -268,9 +275,11 @@ class PgpSignatureChecker { } // check for insecure hash algorithms - if (!PgpSecurityConstants.isSecureHashAlgorithm(onePassSignature.getHashAlgorithm())) { + InsecureHashAlgorithm signatureSecurityProblem = + PgpSecurityConstants.checkSignatureAlgorithmForSecurityProblems(onePassSignature.getHashAlgorithm()); + if (signatureSecurityProblem != null) { log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1); - signatureResultBuilder.setInsecure(true); + signatureResultBuilder.addSecurityProblem(signatureSecurityProblem); } signatureResultBuilder.setSignatureTimestamp(messageSignature.getCreationTime()); @@ -288,6 +297,10 @@ class PgpSignatureChecker { return signatureResultBuilder.build(); } + public List getSecurityProblems() { + return signatureResultBuilder.getSecurityProblems(); + } + /** * Mostly taken from ClearSignedFileProcessor in Bouncy Castle */ diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/SecurityProblem.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/SecurityProblem.java new file mode 100644 index 000000000..58819636a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/SecurityProblem.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.pgp; + + +import java.io.Serializable; + +public abstract class SecurityProblem implements Serializable { + + public static abstract class KeySecurityProblem extends SecurityProblem { + public final long masterKeyId; + public final long subKeyId; + public final int algorithm; + + private KeySecurityProblem(long masterKeyId, long subKeyId, int algorithm) { + this.masterKeyId = masterKeyId; + this.subKeyId = subKeyId; + this.algorithm = algorithm; + } + } + + public static abstract class SymmetricAlgorithmProblem extends SecurityProblem { + + } + + public static class InsecureBitStrength extends KeySecurityProblem { + public final int bitStrength; + + InsecureBitStrength(long masterKeyId, long subKeyId, int algorithm, int bitStrength) { + super(masterKeyId, subKeyId, algorithm); + this.bitStrength = bitStrength; + } + } + + public static class NotWhitelistedCurve extends KeySecurityProblem { + public final String curveOid; + + NotWhitelistedCurve(long masterKeyId, long subKeyId, String curveOid, int algorithm) { + super(masterKeyId, subKeyId, algorithm); + this.curveOid = curveOid; + } + } + + public static class UnidentifiedKeyProblem extends KeySecurityProblem { + UnidentifiedKeyProblem(long masterKeyId, long subKeyId, int algorithm) { + super(masterKeyId, subKeyId, algorithm); + } + } + + public static class InsecureHashAlgorithm extends SecurityProblem { + public final int hashAlgorithm; + + InsecureHashAlgorithm(int hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } + } + + public static class InsecureSymmetricAlgorithm extends SymmetricAlgorithmProblem { + public final int symmetricAlgorithm; + + InsecureSymmetricAlgorithm(int symmetricAlgorithm) { + this.symmetricAlgorithm = symmetricAlgorithm; + } + } + + public static class MissingMdc extends SymmetricAlgorithmProblem { + + } +} 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 536900ba1..1c2bf024f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -111,6 +111,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements public static final String EXTRA_SECURITY_TOKEN_AID = "security_token_aid"; public static final String EXTRA_SECURITY_TOKEN_VERSION = "security_token_version"; public static final String EXTRA_SECURITY_TOKEN_FINGERPRINTS = "security_token_fingerprints"; + private boolean mLinkedTransition; @Retention(RetentionPolicy.SOURCE) @IntDef({REQUEST_QR_FINGERPRINT, REQUEST_BACKUP, REQUEST_CERTIFY, REQUEST_DELETE}) @@ -331,20 +332,13 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements return; } - boolean linkedTransition = getIntent().getBooleanExtra(EXTRA_LINKED_TRANSITION, false); - if (linkedTransition && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mLinkedTransition = getIntent().getBooleanExtra(EXTRA_LINKED_TRANSITION, false); + if (mLinkedTransition && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { postponeEnterTransition(); } - FragmentManager manager = getSupportFragmentManager(); - // Create an instance of the fragment - final ViewKeyFragment frag = ViewKeyFragment.newInstance(mDataUri, - linkedTransition ? PostponeType.LINKED : PostponeType.NONE); - manager.beginTransaction() - .replace(R.id.view_key_fragment, frag) - .commit(); - if (Preferences.getPreferences(this).getExperimentalEnableKeybase()) { + FragmentManager manager = getSupportFragmentManager(); final ViewKeyKeybaseFragment keybaseFrag = ViewKeyKeybaseFragment.newInstance(mDataUri); manager.beginTransaction() .replace(R.id.view_key_keybase_fragment, keybaseFrag) @@ -512,7 +506,7 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements private void certifyImmediate() { Intent intent = new Intent(this, CertifyKeyActivity.class); - intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[]{mMasterKeyId}); + intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[] { mMasterKeyId }); startActivityForResult(intent, REQUEST_CERTIFY); } @@ -734,6 +728,32 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements } + public void showMainFragment() { + new Handler().post(new Runnable() { + @Override + public void run() { + FragmentManager manager = getSupportFragmentManager(); + + // unless we must refresh + ViewKeyFragment frag = (ViewKeyFragment) manager.findFragmentByTag("view_key_fragment"); + // if everything is valid, just drop it + if (frag != null && frag.isValidForData(mIsSecret)) { + return; + } + + // if the main fragment doesn't exist, or is not of the correct type, (re)create it + frag = ViewKeyFragment.newInstance(mMasterKeyId, mIsSecret, + mLinkedTransition ? PostponeType.LINKED : PostponeType.NONE); + // get rid of possible backstack, this fragment is always at the bottom + manager.popBackStack("security_token", FragmentManager.POP_BACK_STACK_INCLUSIVE); + manager.beginTransaction() + .replace(R.id.view_key_fragment, frag, "view_key_fragment") + // if this gets lost, it doesn't really matter since the loader will reinstate it onResume + .commitAllowingStateLoss(); + } + }); + } + private void encrypt(Uri dataUri, boolean text) { // If there is no encryption key, don't bother. if (!mHasEncrypt) { @@ -900,6 +920,15 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID); mFingerprint = data.getBlob(INDEX_FINGERPRINT); mFingerprintString = KeyFormattingUtils.convertFingerprintToHex(mFingerprint); + mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; + mHasEncrypt = data.getInt(INDEX_HAS_ENCRYPT) != 0; + mIsRevoked = data.getInt(INDEX_IS_REVOKED) > 0; + mIsExpired = data.getInt(INDEX_IS_EXPIRED) != 0; + mIsSecure = data.getInt(INDEX_IS_SECURE) == 1; + mIsVerified = data.getInt(INDEX_VERIFIED) > 0; + + // queue showing of the main fragment + showMainFragment(); // if it wasn't shown yet, display token fragment if (mShowSecurityTokenAfterCreation && getIntent().hasExtra(EXTRA_SECURITY_TOKEN_AID)) { @@ -912,13 +941,6 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements showSecurityTokenFragment(tokenFingerprints, tokenUserId, tokenAid, tokenVersion); } - mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; - mHasEncrypt = data.getInt(INDEX_HAS_ENCRYPT) != 0; - mIsRevoked = data.getInt(INDEX_IS_REVOKED) > 0; - mIsExpired = data.getInt(INDEX_IS_EXPIRED) != 0; - mIsSecure = data.getInt(INDEX_IS_SECURE) == 1; - mIsVerified = data.getInt(INDEX_VERIFIED) > 0; - // if the refresh animation isn't playing if (!mRotate.hasStarted() && !mRotateSpin.hasStarted()) { // re-create options menu based on mIsSecret, mIsVerified @@ -944,10 +966,24 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements } }; + boolean showStatusText = mIsSecure && !mIsExpired && !mIsRevoked; + if (showStatusText) { + mStatusText.setVisibility(View.VISIBLE); + + if (mIsSecret) { + mStatusText.setText(R.string.view_key_my_key); + } else if (mIsVerified) { + mStatusText.setText(R.string.view_key_verified); + } else { + mStatusText.setText(R.string.view_key_unverified); + } + } else { + mStatusText.setVisibility(View.GONE); + } + // Note: order is important int color; if (mIsRevoked) { - mStatusText.setText(R.string.view_key_revoked); mStatusImage.setVisibility(View.VISIBLE); KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText, State.REVOKED, R.color.icons, true); @@ -960,7 +996,6 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements hideFab(); mQrCodeLayout.setVisibility(View.GONE); } else if (!mIsSecure) { - mStatusText.setText(R.string.view_key_insecure); mStatusImage.setVisibility(View.VISIBLE); KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText, State.INSECURE, R.color.icons, true); @@ -973,11 +1008,6 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements hideFab(); mQrCodeLayout.setVisibility(View.GONE); } else if (mIsExpired) { - if (mIsSecret) { - mStatusText.setText(R.string.view_key_expired_secret); - } else { - mStatusText.setText(R.string.view_key_expired); - } mStatusImage.setVisibility(View.VISIBLE); KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText, State.EXPIRED, R.color.icons, true); @@ -990,7 +1020,6 @@ public class ViewKeyActivity extends BaseSecurityTokenActivity implements hideFab(); mQrCodeLayout.setVisibility(View.GONE); } else if (mIsSecret) { - mStatusText.setText(R.string.view_key_my_key); mStatusImage.setVisibility(View.GONE); // noinspection deprecation, fix requires api level 23 color = getResources().getColor(R.color.key_flag_green); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java index 81a3a71e5..f3662c1e7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java @@ -18,6 +18,7 @@ package org.sufficientlysecure.keychain.ui; + import java.io.IOException; import java.util.List; @@ -59,6 +60,7 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter; import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; import org.sufficientlysecure.keychain.ui.base.LoaderFragment; @@ -66,14 +68,16 @@ import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment; import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment; import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment.OnIdentityLoadedListener; import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard; +import org.sufficientlysecure.keychain.ui.widget.KeyHealthPresenter; +import org.sufficientlysecure.keychain.ui.widget.KeyHealthCardView; import org.sufficientlysecure.keychain.util.ContactHelper; import org.sufficientlysecure.keychain.util.Log; -import org.sufficientlysecure.keychain.util.Preferences; public class ViewKeyFragment extends LoaderFragment implements LoaderManager.LoaderCallbacks { - public static final String ARG_DATA_URI = "uri"; + public static final String ARG_MASTER_KEY_ID = "master_key_id"; + public static final String ARG_IS_SECRET = "is_secret"; public static final String ARG_POSTPONE_TYPE = "postpone_type"; private ListView mUserIds; @@ -84,20 +88,16 @@ public class ViewKeyFragment extends LoaderFragment implements boolean mIsSecret = false; - private static final int LOADER_ID_UNIFIED = 0; private static final int LOADER_ID_USER_IDS = 1; private static final int LOADER_ID_LINKED_IDS = 2; private static final int LOADER_ID_LINKED_CONTACT = 3; - - private static final String LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID - = "loader_linked_contact_master_key_id"; - private static final String LOADER_EXTRA_LINKED_CONTACT_IS_SECRET - = "loader_linked_contact_is_secret"; + private static final int LOADER_ID_SUBKEY_STATUS = 4; private UserIdsAdapter mUserIdsAdapter; private LinkedIdsAdapter mLinkedIdsAdapter; private Uri mDataUri; + private PostponeType mPostponeType; private CardView mSystemContactCard; @@ -111,13 +111,19 @@ public class ViewKeyFragment extends LoaderFragment implements private byte[] mFingerprint; private TextView mLinkedIdsExpander; + KeyHealthCardView mKeyHealthCard; + KeyHealthPresenter mKeyHealthPresenter; + + private long mMasterKeyId; + /** * Creates new instance of this fragment */ - public static ViewKeyFragment newInstance(Uri dataUri, PostponeType postponeType) { + public static ViewKeyFragment newInstance(long masterKeyId, boolean isSecret, PostponeType postponeType) { ViewKeyFragment frag = new ViewKeyFragment(); Bundle args = new Bundle(); - args.putParcelable(ARG_DATA_URI, dataUri); + args.putLong(ARG_MASTER_KEY_ID, masterKeyId); + args.putBoolean(ARG_IS_SECRET, isSecret); args.putString(ARG_POSTPONE_TYPE, postponeType.toString()); frag.setArguments(args); @@ -169,6 +175,8 @@ public class ViewKeyFragment extends LoaderFragment implements } }); + mKeyHealthCard = (KeyHealthCardView) view.findViewById(R.id.subkey_status_card); + return root; } @@ -223,7 +231,29 @@ public class ViewKeyFragment extends LoaderFragment implements }); } }); + } + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mMasterKeyId = getArguments().getLong(ARG_MASTER_KEY_ID); + mDataUri = KeyRings.buildGenericKeyRingUri(mMasterKeyId); + mIsSecret = getArguments().getBoolean(ARG_IS_SECRET); + mPostponeType = PostponeType.valueOf(getArguments().getString(ARG_POSTPONE_TYPE)); + + // load user ids after we know if it's a secret key + mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !mIsSecret, null); + mUserIds.setAdapter(mUserIdsAdapter); + + // initialize loaders, which will take care of auto-refresh on change + getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); + initLinkedContactLoader(); + initCardButtonsVisibility(mIsSecret); + + mKeyHealthPresenter = new KeyHealthPresenter( + getContext(), mKeyHealthCard, LOADER_ID_SUBKEY_STATUS, mMasterKeyId, mIsSecret); + mKeyHealthPresenter.startLoader(getLoaderManager()); } private void showUserIdInfo(final int position) { @@ -249,7 +279,9 @@ public class ViewKeyFragment extends LoaderFragment implements */ private void loadLinkedSystemContact(final long contactId) { // contact doesn't exist, stop - if (contactId == -1) return; + if (contactId == -1) { + return; + } final Context context = mSystemContactName.getContext(); ContactHelper contactHelper = new ContactHelper(context); @@ -265,7 +297,7 @@ public class ViewKeyFragment extends LoaderFragment implements contactName = contactHelper.getContactName(contactId); } - if (contactName != null) {//contact name exists for given master key + if (contactName != null) { //contact name exists for given master key showLinkedSystemContact(); mSystemContactName.setText(contactName); @@ -311,21 +343,6 @@ public class ViewKeyFragment extends LoaderFragment implements context.startActivity(intent); } - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); - mPostponeType = PostponeType.valueOf(getArguments().getString(ARG_POSTPONE_TYPE)); - if (dataUri == null) { - Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); - getActivity().finish(); - return; - } - - loadData(dataUri); - } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { // if a result has been returned, display a notify @@ -337,58 +354,16 @@ public class ViewKeyFragment extends LoaderFragment implements } } - static final String[] UNIFIED_PROJECTION = new String[]{ - KeychainContract.KeyRings._ID, - KeychainContract.KeyRings.MASTER_KEY_ID, - KeychainContract.KeyRings.USER_ID, - KeychainContract.KeyRings.IS_REVOKED, - KeychainContract.KeyRings.IS_EXPIRED, - KeychainContract.KeyRings.VERIFIED, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeychainContract.KeyRings.FINGERPRINT, - KeychainContract.KeyRings.HAS_ENCRYPT - }; - - static final int INDEX_MASTER_KEY_ID = 1; - @SuppressWarnings("unused") - static final int INDEX_USER_ID = 2; - @SuppressWarnings("unused") - static final int INDEX_IS_REVOKED = 3; - @SuppressWarnings("unused") - static final int INDEX_IS_EXPIRED = 4; - @SuppressWarnings("unused") - static final int INDEX_VERIFIED = 5; - static final int INDEX_HAS_ANY_SECRET = 6; - static final int INDEX_FINGERPRINT = 7; - @SuppressWarnings("unused") - static final int INDEX_HAS_ENCRYPT = 8; - private static final String[] RAW_CONTACT_PROJECTION = { ContactsContract.RawContacts.CONTACT_ID }; private static final int INDEX_CONTACT_ID = 0; - private void loadData(Uri dataUri) { - mDataUri = dataUri; - - Log.i(Constants.TAG, "mDataUri: " + mDataUri); - - // Prepare the loaders. Either re-connect with an existing ones, - // or start new ones. - getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); - } - @Override public Loader onCreateLoader(int id, Bundle args) { switch (id) { - case LOADER_ID_UNIFIED: { - setContentShown(false, false); - Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri); - return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null); - } - case LOADER_ID_USER_IDS: { return UserIdsAdapter.createLoader(getActivity(), mDataUri); } @@ -401,11 +376,7 @@ public class ViewKeyFragment extends LoaderFragment implements // we need a separate loader for linked contact // to ensure refreshing on verification - // passed in args to explicitly specify their need - long masterKeyId = args.getLong(LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID); - boolean isSecret = args.getBoolean(LOADER_EXTRA_LINKED_CONTACT_IS_SECRET); - - Uri baseUri = isSecret ? ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI : + Uri baseUri = mIsSecret ? ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI : ContactsContract.RawContacts.CONTENT_URI; return new CursorLoader( @@ -417,12 +388,16 @@ public class ViewKeyFragment extends LoaderFragment implements ContactsContract.RawContacts.DELETED + "=?", new String[]{ Constants.ACCOUNT_TYPE, - Long.toString(masterKeyId), + Long.toString(mMasterKeyId), "0" // "0" for "not deleted" }, null); } + case LOADER_ID_SUBKEY_STATUS: { + throw new IllegalStateException("This callback should never end up here!"); + } + default: return null; } @@ -439,22 +414,6 @@ public class ViewKeyFragment extends LoaderFragment implements // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) switch (loader.getId()) { - case LOADER_ID_UNIFIED: { - if (data.getCount() == 1 && data.moveToFirst()) { - - mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; - mFingerprint = data.getBlob(INDEX_FINGERPRINT); - long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID); - - // init other things after we know if it's a secret key - initUserIds(mIsSecret); - initLinkedIds(mIsSecret); - initLinkedContactLoader(masterKeyId, mIsSecret); - initCardButtonsVisibility(mIsSecret); - } - break; - } - case LOADER_ID_USER_IDS: { setContentShown(true, false); mUserIdsAdapter.swapCursor(data); @@ -494,25 +453,14 @@ public class ViewKeyFragment extends LoaderFragment implements } break; } + + case LOADER_ID_SUBKEY_STATUS: { + throw new IllegalStateException("This callback should never end up here!"); + } } } - private void initUserIds(boolean isSecret) { - mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !isSecret, null); - mUserIds.setAdapter(mUserIdsAdapter); - getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); - } - - private void initLinkedIds(boolean isSecret) { - if (Preferences.getPreferences(getActivity()).getExperimentalEnableLinkedIdentities()) { - mLinkedIdsAdapter = - new LinkedIdsAdapter(getActivity(), null, 0, isSecret, mLinkedIdsExpander); - mLinkedIds.setAdapter(mLinkedIdsAdapter); - getLoaderManager().initLoader(LOADER_ID_LINKED_IDS, null, this); - } - } - - private void initLinkedContactLoader(long masterKeyId, boolean isSecret) { + private void initLinkedContactLoader() { if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_DENIED) { Log.w(Constants.TAG, "loading linked system contact not possible READ_CONTACTS permission denied!"); @@ -521,8 +469,6 @@ public class ViewKeyFragment extends LoaderFragment implements } Bundle linkedContactData = new Bundle(); - linkedContactData.putLong(LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID, masterKeyId); - linkedContactData.putBoolean(LOADER_EXTRA_LINKED_CONTACT_IS_SECRET, isSecret); // initialises loader for contact query so we can listen to any updates getLoaderManager().initLoader(LOADER_ID_LINKED_CONTACT, linkedContactData, this); @@ -557,7 +503,14 @@ public class ViewKeyFragment extends LoaderFragment implements mLinkedIdsAdapter.swapCursor(null); break; } + case LOADER_ID_SUBKEY_STATUS: + mKeyHealthPresenter.onLoaderReset(loader); + break; } } + public boolean isValidForData(boolean isSecret) { + return isSecret == mIsSecret; + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java index 279fe1832..c6c4b6954 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java @@ -62,6 +62,10 @@ public class KeyFormattingUtils { return getAlgorithmInfo(null, algorithm, keySize, oid); } + public static String getAlgorithmInfo(int algorithm) { + return getAlgorithmInfo(null, algorithm, null, null); + } + /** * Based on OpenPGP Message Format */ diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthCardView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthCardView.java new file mode 100644 index 000000000..2474c8f86 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthCardView.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.widget; + + +import java.util.Date; + +import android.content.Context; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.CardView; +import android.text.format.DateFormat; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.SecurityProblem.InsecureBitStrength; +import org.sufficientlysecure.keychain.pgp.SecurityProblem.KeySecurityProblem; +import org.sufficientlysecure.keychain.pgp.SecurityProblem.NotWhitelistedCurve; +import org.sufficientlysecure.keychain.pgp.SecurityProblem.UnidentifiedKeyProblem; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.ui.widget.KeyHealthPresenter.KeyHealthClickListener; +import org.sufficientlysecure.keychain.ui.widget.KeyHealthPresenter.KeyHealthMvpView; +import org.sufficientlysecure.keychain.ui.widget.KeyHealthPresenter.KeyHealthStatus; +import org.sufficientlysecure.keychain.ui.widget.KeyStatusList.KeyDisplayStatus; + + +public class KeyHealthCardView extends CardView implements KeyHealthMvpView, OnClickListener { + private final View vLayout; + private final TextView vTitle, vSubtitle; + private final ImageView vIcon; + private final ImageView vExpander; + private final KeyStatusList vKeyStatusList; + private final View vKeyStatusDivider; + private final View vInsecureLayout; + private final TextView vInsecureProblem; + private final TextView vInsecureSolution; + private final View vExpiryLayout; + private final TextView vExpiryText; + + private KeyHealthClickListener keyHealthClickListener; + + public KeyHealthCardView(Context context, AttributeSet attrs) { + super(context, attrs); + + View view = LayoutInflater.from(context).inflate(R.layout.key_health_card_content, this, true); + + vLayout = view.findViewById(R.id.key_health_layout); + vTitle = (TextView) view.findViewById(R.id.key_health_title); + vSubtitle = (TextView) view.findViewById(R.id.key_health_subtitle); + vIcon = (ImageView) view.findViewById(R.id.key_health_icon); + vExpander = (ImageView) view.findViewById(R.id.key_health_expander); + + vLayout.setOnClickListener(this); + + vKeyStatusDivider = view.findViewById(R.id.key_health_divider); + vKeyStatusList = (KeyStatusList) view.findViewById(R.id.key_health_status_list); + + vInsecureLayout = view.findViewById(R.id.key_insecure_layout); + vInsecureProblem = (TextView) view.findViewById(R.id.key_insecure_problem); + vInsecureSolution = (TextView) view.findViewById(R.id.key_insecure_solution); + + vExpiryLayout = view.findViewById(R.id.key_expiry_layout); + vExpiryText = (TextView) view.findViewById(R.id.key_expiry_text); + } + + private enum KeyHealthDisplayStatus { + OK (R.string.key_health_ok_title, R.string.key_health_ok_subtitle, + R.drawable.ic_check_black_24dp, R.color.android_green_light), + DIVERT (R.string.key_health_divert_title, R.string.key_health_divert_subtitle, + R.drawable.yubi_icon_24dp, R.color.md_black_1000), + REVOKED (R.string.key_health_revoked_title, R.string.key_health_revoked_subtitle, + R.drawable.ic_close_black_24dp, R.color.android_red_light), + EXPIRED (R.string.key_health_expired_title, R.string.key_health_expired_subtitle, + R.drawable.status_signature_expired_cutout_24dp, R.color.android_red_light), + INSECURE (R.string.key_health_insecure_title, R.string.key_health_insecure_subtitle, + R.drawable.ic_close_black_24dp, R.color.android_red_light), + BROKEN(R.string.key_health_broken_title, R.string.key_health_broken_subtitle, + R.drawable.broken_heart_24dp, R.color.android_red_light), + SIGN_ONLY (R.string.key_health_sign_only_title, R.string.key_health_sign_only_subtitle, + R.drawable.ic_check_black_24dp, R.color.android_green_light), + STRIPPED (R.string.key_health_stripped_title, R.string.key_health_stripped_subtitle, + R.drawable.ic_check_black_24dp, R.color.android_green_light), + PARTIAL_STRIPPED (R.string.key_health_partial_stripped_title, R.string.key_health_partial_stripped_subtitle, + R.drawable.ic_check_black_24dp, R.color.android_green_light); + + @StringRes + private final int title, subtitle; + @DrawableRes + private final int icon; + @ColorRes + private final int iconColor; + + KeyHealthDisplayStatus(@StringRes int title, @StringRes int subtitle, + @DrawableRes int icon, @ColorRes int iconColor) { + this.title = title; + this.subtitle = subtitle; + this.icon = icon; + this.iconColor = iconColor; + } + } + + @Override + public void setKeyStatus(KeyHealthStatus keyHealthStatus) { + switch (keyHealthStatus) { + case OK: + setKeyStatus(KeyHealthDisplayStatus.OK); + break; + case DIVERT: + setKeyStatus(KeyHealthDisplayStatus.DIVERT); + break; + case REVOKED: + setKeyStatus(KeyHealthDisplayStatus.REVOKED); + break; + case EXPIRED: + setKeyStatus(KeyHealthDisplayStatus.EXPIRED); + break; + case INSECURE: + setKeyStatus(KeyHealthDisplayStatus.INSECURE); + break; + case BROKEN: + setKeyStatus(KeyHealthDisplayStatus.BROKEN); + break; + case STRIPPED: + setKeyStatus(KeyHealthDisplayStatus.STRIPPED); + break; + case SIGN_ONLY: + setKeyStatus(KeyHealthDisplayStatus.SIGN_ONLY); + break; + case PARTIAL_STRIPPED: + setKeyStatus(KeyHealthDisplayStatus.PARTIAL_STRIPPED); + break; + } + } + + @Override + public void setPrimarySecurityProblem(KeySecurityProblem securityProblem) { + if (securityProblem == null) { + vInsecureLayout.setVisibility(View.GONE); + return; + } + vInsecureLayout.setVisibility(View.VISIBLE); + + if (securityProblem instanceof InsecureBitStrength) { + InsecureBitStrength insecureBitStrength = (InsecureBitStrength) securityProblem; + vInsecureProblem.setText(getResources().getString(R.string.key_insecure_bitstrength_2048_problem, + KeyFormattingUtils.getAlgorithmInfo(insecureBitStrength.algorithm), + Integer.toString(insecureBitStrength.bitStrength))); + vInsecureSolution.setText(R.string.key_insecure_bitstrength_2048_solution); + } else if (securityProblem instanceof NotWhitelistedCurve) { + NotWhitelistedCurve notWhitelistedCurve = (NotWhitelistedCurve) securityProblem; + + String curveName = KeyFormattingUtils.getCurveInfo(getContext(), notWhitelistedCurve.curveOid); + vInsecureProblem.setText(getResources().getString(R.string.key_insecure_unknown_curve_problem, curveName)); + vInsecureSolution.setText(R.string.key_insecure_unknown_curve_solution); + } else if (securityProblem instanceof UnidentifiedKeyProblem) { + vInsecureProblem.setText(R.string.key_insecure_unidentified_problem); + vInsecureSolution.setText(R.string.key_insecure_unknown_curve_solution); + } else { + throw new IllegalArgumentException("all subclasses of KeySecurityProblem must be handled!"); + } + + } + + @Override + public void setPrimaryExpiryDate(Date expiry) { + if (expiry == null) { + vExpiryLayout.setVisibility(View.GONE); + return; + } + vExpiryLayout.setVisibility(View.VISIBLE); + + String expiryText = DateFormat.getMediumDateFormat(getContext()).format(expiry); + vExpiryText.setText(getResources().getString(R.string.key_expiry_text, expiryText)); + } + + @Override + public void onClick(View view) { + if (keyHealthClickListener != null) { + keyHealthClickListener.onKeyHealthClick(); + } + } + + @Override + public void setOnHealthClickListener(KeyHealthClickListener keyHealthClickListener) { + this.keyHealthClickListener = keyHealthClickListener; + vLayout.setClickable(keyHealthClickListener != null); + } + + @Override + public void setShowExpander(boolean showExpander) { + vLayout.setClickable(showExpander); + vExpander.setVisibility(showExpander ? View.VISIBLE : View.GONE); + } + + @Override + public void showExpandedState(KeyDisplayStatus certifyStatus, KeyDisplayStatus signStatus, + KeyDisplayStatus encryptStatus) { + if (certifyStatus == null && signStatus == null && encryptStatus == null) { + vKeyStatusList.setVisibility(View.GONE); + vKeyStatusDivider.setVisibility(View.GONE); + vExpander.setImageResource(R.drawable.ic_expand_more_black_24dp); + } else { + vKeyStatusList.setVisibility(View.VISIBLE); + vKeyStatusDivider.setVisibility(View.VISIBLE); + vExpander.setImageResource(R.drawable.ic_expand_less_black_24dp); + + vKeyStatusList.setCertifyStatus(certifyStatus); + vKeyStatusList.setSignStatus(signStatus); + vKeyStatusList.setDecryptStatus(encryptStatus); + } + + } + + @Override + public void hideExpandedInfo() { + showExpandedState(null, null, null); + } + + private void setKeyStatus(KeyHealthDisplayStatus keyHealthDisplayStatus) { + vTitle.setText(keyHealthDisplayStatus.title); + vSubtitle.setText(keyHealthDisplayStatus.subtitle); + vIcon.setImageResource(keyHealthDisplayStatus.icon); + vIcon.setColorFilter(ContextCompat.getColor(getContext(), keyHealthDisplayStatus.iconColor)); + + setVisibility(View.VISIBLE); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthPresenter.java new file mode 100644 index 000000000..ae1acc97c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyHealthPresenter.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.widget; + + +import java.util.Comparator; +import java.util.Date; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.Loader; + +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; +import org.sufficientlysecure.keychain.pgp.SecurityProblem.KeySecurityProblem; +import org.sufficientlysecure.keychain.ui.widget.KeyStatusList.KeyDisplayStatus; +import org.sufficientlysecure.keychain.ui.widget.SubkeyStatusLoader.KeySubkeyStatus; +import org.sufficientlysecure.keychain.ui.widget.SubkeyStatusLoader.SubKeyItem; + + +public class KeyHealthPresenter implements LoaderCallbacks { + static final Comparator SUBKEY_COMPARATOR = new Comparator() { + @Override + public int compare(SubKeyItem one, SubKeyItem two) { + // if one is valid and the other isn't, the valid one always comes first + if (one.isValid() ^ two.isValid()) { + return one.isValid() ? -1 : 1; + } + // compare usability, if one is "more usable" than the other, that one comes first + int usability = one.mSecretKeyType.compareUsability(two.mSecretKeyType); + if (usability != 0) { + return usability; + } + if ((one.mSecurityProblem == null) ^ (two.mSecurityProblem == null)) { + return one.mSecurityProblem == null ? -1 : 1; + } + // otherwise, the newer one comes first + return one.newerThan(two) ? -1 : 1; + } + }; + + private final Context context; + private final KeyHealthMvpView view; + private final int loaderId; + + private final long masterKeyId; + private final boolean isSecret; + + private KeySubkeyStatus subkeyStatus; + private boolean showingExpandedInfo; + + + public KeyHealthPresenter(Context context, KeyHealthMvpView view, int loaderId, long masterKeyId, boolean isSecret) { + this.context = context; + this.view = view; + this.loaderId = loaderId; + + this.masterKeyId = masterKeyId; + this.isSecret = isSecret; + + view.setOnHealthClickListener(new KeyHealthClickListener() { + @Override + public void onKeyHealthClick() { + KeyHealthPresenter.this.onKeyHealthClick(); + } + }); + } + + public void startLoader(LoaderManager loaderManager) { + loaderManager.restartLoader(loaderId, null, this); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new SubkeyStatusLoader(context, context.getContentResolver(), masterKeyId, SUBKEY_COMPARATOR); + } + + @Override + public void onLoadFinished(Loader loader, KeySubkeyStatus subkeyStatus) { + this.subkeyStatus = subkeyStatus; + + KeyHealthStatus keyHealthStatus = determineKeyHealthStatus(subkeyStatus); + + boolean isInsecure = keyHealthStatus == KeyHealthStatus.INSECURE; + boolean isExpired = keyHealthStatus == KeyHealthStatus.EXPIRED; + if (isInsecure) { + boolean primaryKeySecurityProblem = subkeyStatus.keyCertify.mSecurityProblem != null; + if (primaryKeySecurityProblem) { + view.setKeyStatus(keyHealthStatus); + view.setPrimarySecurityProblem(subkeyStatus.keyCertify.mSecurityProblem); + view.setShowExpander(false); + } else { + view.setKeyStatus(keyHealthStatus); + view.setShowExpander(false); + displayExpandedInfo(false); + } + } else if (isExpired) { + view.setKeyStatus(keyHealthStatus); + view.setPrimaryExpiryDate(subkeyStatus.keyCertify.mExpiry); + view.setShowExpander(false); + } else { + view.setKeyStatus(keyHealthStatus); + view.setShowExpander(keyHealthStatus != KeyHealthStatus.REVOKED); + } + } + + private KeyHealthStatus determineKeyHealthStatus(KeySubkeyStatus subkeyStatus) { + SubKeyItem keyCertify = subkeyStatus.keyCertify; + if (keyCertify.mIsRevoked) { + return KeyHealthStatus.REVOKED; + } + + if (keyCertify.mIsExpired) { + return KeyHealthStatus.EXPIRED; + } + + if (keyCertify.mSecurityProblem != null) { + return KeyHealthStatus.INSECURE; + } + + if (!subkeyStatus.keysSign.isEmpty() && subkeyStatus.keysEncrypt.isEmpty()) { + SubKeyItem keySign = subkeyStatus.keysSign.get(0); + if (!keySign.isValid()) { + return KeyHealthStatus.BROKEN; + } + + if (keySign.mSecurityProblem != null) { + return KeyHealthStatus.INSECURE; + } + + return KeyHealthStatus.SIGN_ONLY; + } + + if (subkeyStatus.keysSign.isEmpty() || subkeyStatus.keysEncrypt.isEmpty()) { + return KeyHealthStatus.BROKEN; + } + + SubKeyItem keySign = subkeyStatus.keysSign.get(0); + SubKeyItem keyEncrypt = subkeyStatus.keysEncrypt.get(0); + + if (keySign.mSecurityProblem != null && keySign.isValid() + || keyEncrypt.mSecurityProblem != null && keyEncrypt.isValid()) { + return KeyHealthStatus.INSECURE; + } + + if (!keySign.isValid() || !keyEncrypt.isValid()) { + return KeyHealthStatus.BROKEN; + } + + if (keyCertify.mSecretKeyType == SecretKeyType.GNU_DUMMY + && keySign.mSecretKeyType == SecretKeyType.GNU_DUMMY + && keyEncrypt.mSecretKeyType == SecretKeyType.GNU_DUMMY) { + return KeyHealthStatus.STRIPPED; + } + + if (keyCertify.mSecretKeyType == SecretKeyType.GNU_DUMMY + || keySign.mSecretKeyType == SecretKeyType.GNU_DUMMY + || keyEncrypt.mSecretKeyType == SecretKeyType.GNU_DUMMY) { + return KeyHealthStatus.PARTIAL_STRIPPED; + } + + if (keyCertify.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD + && keySign.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD + && keyEncrypt.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD) { + return KeyHealthStatus.DIVERT; + } + + return KeyHealthStatus.OK; + } + + @Override + public void onLoaderReset(Loader loader) { + + } + + private void onKeyHealthClick() { + if (showingExpandedInfo) { + showingExpandedInfo = false; + view.hideExpandedInfo(); + } else { + showingExpandedInfo = true; + displayExpandedInfo(true); + } + } + + private void displayExpandedInfo(boolean displayAll) { + SubKeyItem keyCertify = subkeyStatus.keyCertify; + SubKeyItem keySign = subkeyStatus.keysSign.isEmpty() ? null : subkeyStatus.keysSign.get(0); + SubKeyItem keyEncrypt = subkeyStatus.keysEncrypt.isEmpty() ? null : subkeyStatus.keysEncrypt.get(0); + + KeyDisplayStatus certDisplayStatus = getKeyDisplayStatus(keyCertify); + KeyDisplayStatus signDisplayStatus = getKeyDisplayStatus(keySign); + KeyDisplayStatus encryptDisplayStatus = getKeyDisplayStatus(keyEncrypt); + + if (!displayAll) { + if (certDisplayStatus == KeyDisplayStatus.OK) { + certDisplayStatus = null; + } + if (certDisplayStatus == KeyDisplayStatus.INSECURE) { + signDisplayStatus = null; + encryptDisplayStatus = null; + } + if (signDisplayStatus == KeyDisplayStatus.OK) { + signDisplayStatus = null; + } + if (encryptDisplayStatus == KeyDisplayStatus.OK) { + encryptDisplayStatus = null; + } + } + + view.showExpandedState(certDisplayStatus, signDisplayStatus, encryptDisplayStatus); + } + + private KeyDisplayStatus getKeyDisplayStatus(SubKeyItem subKeyItem) { + if (subKeyItem == null) { + return KeyDisplayStatus.UNAVAILABLE; + } + + if (subKeyItem.mIsRevoked) { + return KeyDisplayStatus.REVOKED; + } + if (subKeyItem.mIsExpired) { + return KeyDisplayStatus.EXPIRED; + } + if (subKeyItem.mSecurityProblem != null) { + return KeyDisplayStatus.INSECURE; + } + if (subKeyItem.mSecretKeyType == SecretKeyType.GNU_DUMMY) { + return KeyDisplayStatus.STRIPPED; + } + if (subKeyItem.mSecretKeyType == SecretKeyType.DIVERT_TO_CARD) { + return KeyDisplayStatus.DIVERT; + } + + return KeyDisplayStatus.OK; + } + + enum KeyHealthStatus { + OK, DIVERT, REVOKED, EXPIRED, INSECURE, SIGN_ONLY, STRIPPED, PARTIAL_STRIPPED, BROKEN + } + + interface KeyHealthMvpView { + void setKeyStatus(KeyHealthStatus keyHealthStatus); + void setPrimarySecurityProblem(KeySecurityProblem securityProblem); + void setPrimaryExpiryDate(Date expiry); + + void setShowExpander(boolean showExpander); + void showExpandedState(KeyDisplayStatus certifyStatus, KeyDisplayStatus signStatus, + KeyDisplayStatus encryptStatus); + void hideExpandedInfo(); + + void setOnHealthClickListener(KeyHealthClickListener keyHealthClickListener); + + } + + interface KeyStatusMvpView { + void setCertifyStatus(KeyDisplayStatus unavailable); + void setSignStatus(KeyDisplayStatus signStatus); + void setDecryptStatus(KeyDisplayStatus encryptStatus); + } + + interface KeyHealthClickListener { + void onKeyHealthClick(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyStatusList.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyStatusList.java new file mode 100644 index 000000000..171891f39 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyStatusList.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.widget; + + +import android.content.Context; +import android.support.annotation.ColorRes; +import android.support.annotation.StringRes; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.widget.KeyHealthPresenter.KeyStatusMvpView; + + +public class KeyStatusList extends LinearLayout implements KeyStatusMvpView { + private final TextView vCertText, vSignText, vDecryptText; + private final ImageView vCertIcon, vSignIcon, vDecryptIcon; + private final View vCertToken, vSignToken, vDecryptToken; + private final View vCertifyLayout, vSignLayout, vDecryptLayout; + + public KeyStatusList(Context context, AttributeSet attrs) { + super(context, attrs); + + setOrientation(VERTICAL); + + View view = LayoutInflater.from(context).inflate(R.layout.subkey_status_card_content, this, true); + + vCertifyLayout = view.findViewById(R.id.cap_certify); + vSignLayout = view.findViewById(R.id.cap_sign); + vDecryptLayout = view.findViewById(R.id.cap_decrypt); + + vCertText = (TextView) view.findViewById(R.id.cap_cert_text); + vSignText = (TextView) view.findViewById(R.id.cap_sign_text); + vDecryptText = (TextView) view.findViewById(R.id.cap_decrypt_text); + + vCertIcon = (ImageView) view.findViewById(R.id.cap_cert_icon); + vSignIcon = (ImageView) view.findViewById(R.id.cap_sign_icon); + vDecryptIcon = (ImageView) view.findViewById(R.id.cap_decrypt_icon); + + vCertToken = view.findViewById(R.id.cap_cert_security_token); + vSignToken = view.findViewById(R.id.cap_sign_security_token); + vDecryptToken = view.findViewById(R.id.cap_decrypt_security_token); + + } + + // this is just a list of statuses a key can be in, which we can also display + enum KeyDisplayStatus { + OK (R.color.android_green_light, R.color.primary, + R.string.cap_cert_ok, R.string.cap_sign_ok, R.string.cap_decrypt_ok, false), + DIVERT (R.color.android_green_light, R.color.primary, + R.string.cap_cert_divert, R.string.cap_sign_divert, R.string.cap_decrypt_divert, true), + REVOKED (R.color.android_red_light, R.color.android_red_light, + R.string.cap_sign_revoked, R.string.cap_decrypt_revoked, false), + EXPIRED (R.color.android_red_light, R.color.android_red_light, + R.string.cap_sign_expired, R.string.cap_decrypt_expired, false), + STRIPPED (R.color.android_red_light, R.color.android_red_light, + R.string.cap_cert_stripped, R.string.cap_sign_stripped, R.string.cap_decrypt_stripped, false), + INSECURE (R.color.android_red_light, R.color.android_red_light, + R.string.cap_sign_insecure, R.string.cap_sign_insecure, false), + UNAVAILABLE (R.color.android_red_light, R.color.android_red_light, + R.string.cap_cert_unavailable, R.string.cap_sign_unavailable, R.string.cap_decrypt_unavailable, false); + + @ColorRes final int mColor, mTextColor; + @StringRes final Integer mCertifyStr, mSignStr, mDecryptStr; + final boolean mIsDivert; + + KeyDisplayStatus(@ColorRes int color, @ColorRes int textColor, + @StringRes int signStr, @StringRes int encryptStr, boolean isDivert) { + mColor = color; + mTextColor = textColor; + mCertifyStr = null; + mSignStr = signStr; + mDecryptStr = encryptStr; + mIsDivert = isDivert; + } + + KeyDisplayStatus(@ColorRes int color, @ColorRes int textColor, + @StringRes int certifyStr, @StringRes int signStr, @StringRes int encryptStr, boolean isDivert) { + mColor = color; + mTextColor = textColor; + mCertifyStr = certifyStr; + mSignStr = signStr; + mDecryptStr = encryptStr; + mIsDivert = isDivert; + } + + } + + @Override + public void setCertifyStatus(KeyDisplayStatus keyDisplayStatus) { + if (keyDisplayStatus == null) { + vCertifyLayout.setVisibility(View.GONE); + return; + } + + vCertIcon.setColorFilter(getResources().getColor(keyDisplayStatus.mColor)); + vCertText.setText(keyDisplayStatus.mCertifyStr); + vCertText.setTextColor(getResources().getColor(keyDisplayStatus.mTextColor)); + vCertToken.setVisibility(keyDisplayStatus.mIsDivert ? View.VISIBLE : View.GONE); + vCertifyLayout.setVisibility(View.VISIBLE); + } + + @Override + public void setSignStatus(KeyDisplayStatus keyDisplayStatus) { + if (keyDisplayStatus == null) { + vSignLayout.setVisibility(View.GONE); + return; + } + vSignIcon.setColorFilter(getResources().getColor(keyDisplayStatus.mColor)); + vSignText.setText(keyDisplayStatus.mSignStr); + vSignText.setTextColor(getResources().getColor(keyDisplayStatus.mTextColor)); + vSignToken.setVisibility(keyDisplayStatus.mIsDivert ? View.VISIBLE : View.GONE); + vSignLayout.setVisibility(View.VISIBLE); + } + + @Override + public void setDecryptStatus(KeyDisplayStatus keyDisplayStatus) { + if (keyDisplayStatus == null) { + vDecryptLayout.setVisibility(View.GONE); + return; + } + vDecryptIcon.setColorFilter(getResources().getColor(keyDisplayStatus.mColor)); + vDecryptText.setText(keyDisplayStatus.mDecryptStr); + vDecryptText.setTextColor(getResources().getColor(keyDisplayStatus.mTextColor)); + vDecryptToken.setVisibility(keyDisplayStatus.mIsDivert ? View.VISIBLE : View.GONE); + vDecryptLayout.setVisibility(View.VISIBLE); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SubkeyStatusLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SubkeyStatusLoader.java new file mode 100644 index 000000000..0a82938b6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SubkeyStatusLoader.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.widget; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.support.annotation.NonNull; +import android.support.v4.content.AsyncTaskLoader; +import android.util.Log; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; +import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants; +import org.sufficientlysecure.keychain.pgp.SecurityProblem.KeySecurityProblem; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.ui.widget.SubkeyStatusLoader.KeySubkeyStatus; + + +class SubkeyStatusLoader extends AsyncTaskLoader { + public static final String[] PROJECTION = new String[] { + Keys.KEY_ID, + Keys.CREATION, + Keys.CAN_CERTIFY, + Keys.CAN_SIGN, + Keys.CAN_ENCRYPT, + Keys.HAS_SECRET, + Keys.EXPIRY, + Keys.IS_REVOKED, + Keys.ALGORITHM, + Keys.KEY_SIZE, + Keys.KEY_CURVE_OID + }; + private static final int INDEX_KEY_ID = 0; + private static final int INDEX_CREATION = 1; + private static final int INDEX_CAN_CERTIFY = 2; + private static final int INDEX_CAN_SIGN = 3; + private static final int INDEX_CAN_ENCRYPT = 4; + private static final int INDEX_HAS_SECRET = 5; + private static final int INDEX_EXPIRY = 6; + private static final int INDEX_IS_REVOKED = 7; + private static final int INDEX_ALGORITHM = 8; + private static final int INDEX_KEY_SIZE = 9; + private static final int INDEX_KEY_CURVE_OID = 10; + + + private final ContentResolver contentResolver; + private final long masterKeyId; + private final Comparator comparator; + + private KeySubkeyStatus cachedResult; + + + SubkeyStatusLoader(Context context, ContentResolver contentResolver, long masterKeyId, Comparator comparator) { + super(context); + + this.contentResolver = contentResolver; + this.masterKeyId = masterKeyId; + this.comparator = comparator; + } + + @Override + public KeySubkeyStatus loadInBackground() { + Cursor cursor = contentResolver.query(Keys.buildKeysUri(masterKeyId), PROJECTION, null, null, null); + if (cursor == null) { + Log.e(Constants.TAG, "Error loading key items!"); + return null; + } + + try { + SubKeyItem keyCertify = null; + ArrayList keysSign = new ArrayList<>(); + ArrayList keysEncrypt = new ArrayList<>(); + while (cursor.moveToNext()) { + SubKeyItem ski = new SubKeyItem(masterKeyId, cursor); + + if (ski.mKeyId == masterKeyId) { + keyCertify = ski; + } + + if (ski.mCanSign) { + keysSign.add(ski); + } + if (ski.mCanEncrypt) { + keysEncrypt.add(ski); + } + } + + if (keyCertify == null) { + throw new IllegalStateException("Certification key must be set at this point, it's a bug otherwise!"); + } + + Collections.sort(keysSign, comparator); + Collections.sort(keysEncrypt, comparator); + + return new KeySubkeyStatus(keyCertify, keysSign, keysEncrypt); + } finally { + cursor.close(); + } + } + + @Override + public void deliverResult(KeySubkeyStatus keySubkeyStatus) { + cachedResult = keySubkeyStatus; + + if (isStarted()) { + super.deliverResult(keySubkeyStatus); + } + } + + @Override + protected void onStartLoading() { + if (cachedResult != null) { + deliverResult(cachedResult); + } + + if (takeContentChanged() || cachedResult == null) { + forceLoad(); + } + } + + static class KeySubkeyStatus { + @NonNull + final SubKeyItem keyCertify; + final List keysSign; + final List keysEncrypt; + + KeySubkeyStatus(@NonNull SubKeyItem keyCertify, List keysSign, List keysEncrypt) { + this.keyCertify = keyCertify; + this.keysSign = keysSign; + this.keysEncrypt = keysEncrypt; + } + } + + static class SubKeyItem { + final int mPosition; + final long mKeyId; + final Date mCreation; + final SecretKeyType mSecretKeyType; + final boolean mIsRevoked, mIsExpired; + final Date mExpiry; + final boolean mCanCertify, mCanSign, mCanEncrypt; + final KeySecurityProblem mSecurityProblem; + + SubKeyItem(long masterKeyId, Cursor cursor) { + mPosition = cursor.getPosition(); + + mKeyId = cursor.getLong(INDEX_KEY_ID); + mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000); + + mSecretKeyType = SecretKeyType.fromNum(cursor.getInt(INDEX_HAS_SECRET)); + + mIsRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; + mExpiry = cursor.isNull(INDEX_EXPIRY) ? null : new Date(cursor.getLong(INDEX_EXPIRY) * 1000); + mIsExpired = mExpiry != null && mExpiry.before(new Date()); + + mCanCertify = cursor.getInt(INDEX_CAN_CERTIFY) > 0; + mCanSign = cursor.getInt(INDEX_CAN_SIGN) > 0; + mCanEncrypt = cursor.getInt(INDEX_CAN_ENCRYPT) > 0; + + int algorithm = cursor.getInt(INDEX_ALGORITHM); + Integer bitStrength = cursor.isNull(INDEX_KEY_SIZE) ? null : cursor.getInt(INDEX_KEY_SIZE); + String curveOid = cursor.getString(INDEX_KEY_CURVE_OID); + + mSecurityProblem = PgpSecurityConstants.getKeySecurityProblem( + masterKeyId, mKeyId, algorithm, bitStrength, curveOid); + } + + boolean newerThan(SubKeyItem other) { + return mCreation.after(other.mCreation); + } + + boolean isValid() { + return !mIsRevoked && !mIsExpired; + } + } +} diff --git a/OpenKeychain/src/main/res/drawable-hdpi/broken_heart_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/broken_heart_24dp.png new file mode 100644 index 000000000..169f09dbf Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-hdpi/broken_heart_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-mdpi/broken_heart_24dp.png b/OpenKeychain/src/main/res/drawable-mdpi/broken_heart_24dp.png new file mode 100644 index 000000000..d727a5ba4 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-mdpi/broken_heart_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/broken_heart_24dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/broken_heart_24dp.png new file mode 100644 index 000000000..b938c5ce7 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xhdpi/broken_heart_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/broken_heart_24dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/broken_heart_24dp.png new file mode 100644 index 000000000..c64b545a5 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxhdpi/broken_heart_24dp.png differ diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/broken_heart_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/broken_heart_24dp.png new file mode 100644 index 000000000..e068bf777 Binary files /dev/null and b/OpenKeychain/src/main/res/drawable-xxxhdpi/broken_heart_24dp.png differ diff --git a/OpenKeychain/src/main/res/layout/key_health_card_content.xml b/OpenKeychain/src/main/res/layout/key_health_card_content.xml new file mode 100644 index 000000000..1f252ae0d --- /dev/null +++ b/OpenKeychain/src/main/res/layout/key_health_card_content.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OpenKeychain/src/main/res/layout/log_display_activity.xml b/OpenKeychain/src/main/res/layout/log_display_activity.xml index be08ef6fd..16888f81a 100644 --- a/OpenKeychain/src/main/res/layout/log_display_activity.xml +++ b/OpenKeychain/src/main/res/layout/log_display_activity.xml @@ -13,5 +13,4 @@ android:name="org.sufficientlysecure.keychain.ui.LogDisplayFragment" android:layout_width="match_parent" android:layout_height="match_parent" /> - \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/subkey_status_card_content.xml b/OpenKeychain/src/main/res/layout/subkey_status_card_content.xml new file mode 100644 index 000000000..51f980ad5 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/subkey_status_card_content.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OpenKeychain/src/main/res/layout/tools_vertlin.xml b/OpenKeychain/src/main/res/layout/tools_vertlin.xml new file mode 100644 index 000000000..d4f26b18f --- /dev/null +++ b/OpenKeychain/src/main/res/layout/tools_vertlin.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/view_key_fragment.xml b/OpenKeychain/src/main/res/layout/view_key_fragment.xml index aa3a8e8da..fbf910543 100644 --- a/OpenKeychain/src/main/res/layout/view_key_fragment.xml +++ b/OpenKeychain/src/main/res/layout/view_key_fragment.xml @@ -9,6 +9,19 @@ android:paddingRight="16dp" android:paddingTop="16dp"> + + "Help" "Backup key" "Delete key" + "View key status" "Manage my keys" "Search" "Open" @@ -815,10 +816,6 @@ Authentication key - "Revoked: Key must not be used anymore!" - "Insecure: Key must not be used anymore!" - "Expired: The contact needs to extend the key's validity!" - "Expired: You can extend the keys validity by editing it!" "My Key" "Confirmed Key" "Unconfirmed: Scan QR Code to confirm key!" @@ -1798,4 +1795,62 @@ Requested key: Error selecting key %s for signing! Error selecting key %s for encryption! + + "Key Status" + This key is yours. You can use it to: + + Confirm other keys + "This key can confirm other keys." + "This key can confirm other keys, using a Security Token." + "This key is stripped, it can NOT confirm other keys." + "This key is not configured to confirm keys!" + + Sign messages + "This key can sign/send messages." + "This key can sign/send messages, using a Security Token." + "This key can't sign/send messages, because it is expired." + "This key can't sign/send messages, because it is revoked." + "This key can\'t sign/send messages on this device!" + "This key is not configured to sign/send messages!" + "This key can sign/send messages, but not securely!" + + Decrypt messages + "This key can decrypt/receive messages." + "This key can decrypt/receive messages, using a Security Token." + "This key can decrypt/receive messages, but is expired." + "This key can decrypt/receive messages, but is revoked." + "This key can\'t decrypt/receive messages on this device." + "This key is not configured to decrypt/receive messages!" + "This key can decrypt/receive messages, but not securely!" + + "Healthy" + "No key issues found." + "Healthy (Security Token)" + "No key issues found." + "Expired" + "This key should not be used anymore." + "Revoked" + "This key can\'t be used anymore." + "Insecure" + "This key is not secure!" + "Defective" + "Click for details" + "Healthy (Signing Key)" + "Click for details" + "Healthy (Stripped)" + "Click for details" + "Healthy (Partially Stripped)" + "Click for details" + + "This key uses the %1$s algorithm with a strength of %2$s bits. A secure key should have a strength of 2048 bits." + "This key can\'t be upgraded. For secure communication, the owner must generate a new key." + + "This key uses the %1$s algorithm, which is not whitelisted." + "This key can\'t be upgraded. For secure communication, the owner must generate a new key." + + "There is an unidentified problem with this key." + "Please submit a bug report." + + "This key expired on %1$s." + diff --git a/graphics/drawables/broken_heart.svg b/graphics/drawables/broken_heart.svg new file mode 100644 index 000000000..a1cda629b --- /dev/null +++ b/graphics/drawables/broken_heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/graphics/update-drawables.sh b/graphics/update-drawables.sh index 18ed1b63a..4407315b7 100755 --- a/graphics/update-drawables.sh +++ b/graphics/update-drawables.sh @@ -22,7 +22,7 @@ SRC_DIR=./drawables/ #inkscape -w 512 -h 512 -e "$PLAY_DIR/$NAME.png" $NAME.svg -for NAME in "ic_cloud_search" "ic_action_encrypt_file" "ic_action_encrypt_text" "ic_action_verified_cutout" "ic_action_encrypt_copy" "ic_action_encrypt_paste" "ic_action_encrypt_save" "ic_action_encrypt_share" "status_lock_closed" "status_lock_error" "status_lock_open" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "key_flag_authenticate" "key_flag_certify" "key_flag_encrypt" "key_flag_sign" "yubi_icon" "ic_stat_notify" "status_signature_verified_inner" "link" "octo_link" +for NAME in "broken_heart" "ic_cloud_search" "ic_action_encrypt_file" "ic_action_encrypt_text" "ic_action_verified_cutout" "ic_action_encrypt_copy" "ic_action_encrypt_paste" "ic_action_encrypt_save" "ic_action_encrypt_share" "status_lock_closed" "status_lock_error" "status_lock_open" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout" "key_flag_authenticate" "key_flag_certify" "key_flag_encrypt" "key_flag_sign" "yubi_icon" "ic_stat_notify" "status_signature_verified_inner" "link" "octo_link" do echo $NAME inkscape -w 24 -h 24 -e "$MDPI_DIR/${NAME}_24dp.png" "$SRC_DIR/$NAME.svg"