aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
diff options
context:
space:
mode:
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java')
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java347
1 files changed, 307 insertions, 40 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
index d05ce3d5c..681aff56d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
@@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.pgp;
import org.spongycastle.bcpg.ArmoredOutputStream;
import org.spongycastle.bcpg.PublicKeyAlgorithmTags;
import org.spongycastle.bcpg.SignatureSubpacketTags;
+import org.spongycastle.bcpg.UserAttributeSubpacketTags;
import org.spongycastle.bcpg.sig.KeyFlags;
import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPObjectFactory;
@@ -30,18 +31,18 @@ import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.spongycastle.openpgp.PGPSignature;
import org.spongycastle.openpgp.PGPSignatureList;
+import org.spongycastle.openpgp.PGPUserAttributeSubpacketVector;
import org.spongycastle.openpgp.PGPUtil;
import org.spongycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.sufficientlysecure.keychain.Constants;
-import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Utf8Util;
-import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -49,11 +50,13 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
+import java.util.TimeZone;
import java.util.TreeSet;
/** Wrapper around PGPKeyRing class, to be constructed from bytes.
@@ -136,7 +139,7 @@ public class UncachedKeyRing {
public static UncachedKeyRing decodeFromData(byte[] data)
throws PgpGeneralException, IOException {
- Iterator<UncachedKeyRing> parsed = fromStream(new ByteArrayInputStream(data));
+ IteratorWithIOThrow<UncachedKeyRing> parsed = fromStream(new ByteArrayInputStream(data));
if ( ! parsed.hasNext()) {
throw new PgpGeneralException("Object not recognized as PGPKeyRing!");
@@ -152,14 +155,14 @@ public class UncachedKeyRing {
}
- public static Iterator<UncachedKeyRing> fromStream(final InputStream stream) throws IOException {
+ public static IteratorWithIOThrow<UncachedKeyRing> fromStream(final InputStream stream) {
- return new Iterator<UncachedKeyRing>() {
+ return new IteratorWithIOThrow<UncachedKeyRing>() {
UncachedKeyRing mNext = null;
PGPObjectFactory mObjectFactory = null;
- private void cacheNext() {
+ private void cacheNext() throws IOException {
if (mNext != null) {
return;
}
@@ -188,21 +191,19 @@ public class UncachedKeyRing {
// if we are past the while loop, that means the objectFactory had no next
mObjectFactory = null;
}
- } catch (IOException e) {
- Log.e(Constants.TAG, "IOException while processing stream. ArmoredInputStream CRC check failed?", e);
} catch (ArrayIndexOutOfBoundsException e) {
- Log.e(Constants.TAG, "ArmoredInputStream decode failed, symbol is not in decodingTable!", e);
+ throw new IOException(e);
}
}
@Override
- public boolean hasNext() {
+ public boolean hasNext() throws IOException {
cacheNext();
return mNext != null;
}
@Override
- public UncachedKeyRing next() {
+ public UncachedKeyRing next() throws IOException {
try {
cacheNext();
return mNext;
@@ -210,15 +211,15 @@ public class UncachedKeyRing {
mNext = null;
}
}
-
- @Override
- public void remove() {
- throw new UnsupportedOperationException();
- }
};
}
+ public interface IteratorWithIOThrow<E> {
+ public boolean hasNext() throws IOException;
+ public E next() throws IOException;
+ }
+
public void encodeArmored(OutputStream out, String version) throws IOException {
ArmoredOutputStream aos = new ArmoredOutputStream(out);
if (version != null) {
@@ -265,6 +266,35 @@ public class UncachedKeyRing {
*/
@SuppressWarnings("ConstantConditions")
public CanonicalizedKeyRing canonicalize(OperationLog log, int indent) {
+ return canonicalize(log, indent, false);
+ }
+
+
+ /** "Canonicalizes" a public key, removing inconsistencies in the process.
+ *
+ * More specifically:
+ * - Remove all non-verifying self-certificates
+ * - Remove all "future" self-certificates
+ * - Remove all certificates flagged as "local"
+ * - Remove all certificates which are superseded by a newer one on the same target,
+ * including revocations with later re-certifications.
+ * - Remove all certificates in other positions if not of known type:
+ * - key revocation signatures on the master key
+ * - subkey binding signatures for subkeys
+ * - certifications and certification revocations for user ids
+ * - If a subkey retains no valid subkey binding certificate, remove it
+ * - If a user id retains no valid self certificate, remove it
+ * - If the key is a secret key, remove all certificates by foreign keys
+ * - If no valid user id remains, log an error and return null
+ *
+ * This operation writes an OperationLog which can be used as part of an OperationResultParcel.
+ *
+ * @param forExport if this is true, non-exportable signatures will be removed
+ * @return A canonicalized key, or null on fatal error (log will include a message in this case)
+ *
+ */
+ @SuppressWarnings("ConstantConditions")
+ public CanonicalizedKeyRing canonicalize(OperationLog log, int indent, boolean forExport) {
log.add(isSecret() ? LogType.MSG_KC_SECRET : LogType.MSG_KC_PUBLIC,
indent, KeyFormattingUtils.convertKeyIdToHex(getMasterKeyId()));
@@ -276,7 +306,10 @@ public class UncachedKeyRing {
return null;
}
- final Date now = new Date();
+ Calendar nowCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ // allow for diverging clocks up to one day when checking creation time
+ nowCal.add(Calendar.DAY_OF_YEAR, 1);
+ final Date nowPlusOneDay = nowCal.getTime();
int redundantCerts = 0, badCerts = 0;
@@ -297,6 +330,7 @@ public class UncachedKeyRing {
PGPPublicKey modified = masterKey;
PGPSignature revocation = null;
+ PGPSignature notation = null;
for (PGPSignature zert : new IterableIterator<PGPSignature>(masterKey.getKeySignatures())) {
int type = zert.getSignatureType();
@@ -306,47 +340,80 @@ public class UncachedKeyRing {
|| type == PGPSignature.CASUAL_CERTIFICATION
|| type == PGPSignature.POSITIVE_CERTIFICATION
|| type == PGPSignature.CERTIFICATION_REVOCATION) {
- log.add(LogType.MSG_KC_REVOKE_BAD_TYPE_UID, indent);
+ log.add(LogType.MSG_KC_MASTER_BAD_TYPE_UID, indent);
modified = PGPPublicKey.removeCertification(modified, zert);
badCerts += 1;
continue;
}
WrappedSignature cert = new WrappedSignature(zert);
- if (type != PGPSignature.KEY_REVOCATION) {
+ if (type != PGPSignature.KEY_REVOCATION && type != PGPSignature.DIRECT_KEY) {
// Unknown type, just remove
- log.add(LogType.MSG_KC_REVOKE_BAD_TYPE, indent, "0x" + Integer.toString(type, 16));
+ log.add(LogType.MSG_KC_MASTER_BAD_TYPE, indent, "0x" + Integer.toString(type, 16));
modified = PGPPublicKey.removeCertification(modified, zert);
badCerts += 1;
continue;
}
- if (cert.getCreationTime().after(now)) {
+ if (cert.getCreationTime().after(nowPlusOneDay)) {
// Creation date in the future? No way!
- log.add(LogType.MSG_KC_REVOKE_BAD_TIME, indent);
+ log.add(LogType.MSG_KC_MASTER_BAD_TIME, indent);
modified = PGPPublicKey.removeCertification(modified, zert);
badCerts += 1;
continue;
}
- if (cert.isLocal()) {
- // Remove revocation certs with "local" flag
- log.add(LogType.MSG_KC_REVOKE_BAD_LOCAL, indent);
+ try {
+ cert.init(masterKey);
+ if (!cert.verifySignature(masterKey)) {
+ log.add(LogType.MSG_KC_MASTER_BAD, indent);
+ modified = PGPPublicKey.removeCertification(modified, zert);
+ badCerts += 1;
+ continue;
+ }
+ } catch (PgpGeneralException e) {
+ log.add(LogType.MSG_KC_MASTER_BAD_ERR, indent);
modified = PGPPublicKey.removeCertification(modified, zert);
badCerts += 1;
continue;
}
- try {
- cert.init(masterKey);
- if (!cert.verifySignature(masterKey)) {
- log.add(LogType.MSG_KC_REVOKE_BAD, indent);
+ // if this is for export, we always remove any non-exportable certs
+ if (forExport && cert.isLocal()) {
+ // Remove revocation certs with "local" flag
+ log.add(LogType.MSG_KC_MASTER_LOCAL, indent);
+ modified = PGPPublicKey.removeCertification(modified, zert);
+ continue;
+ }
+
+ // special case: non-exportable, direct key signatures for notations!
+ if (cert.getSignatureType() == PGPSignature.DIRECT_KEY) {
+ // must be local, otherwise strip!
+ if (!cert.isLocal()) {
+ log.add(LogType.MSG_KC_MASTER_BAD_TYPE, indent);
modified = PGPPublicKey.removeCertification(modified, zert);
badCerts += 1;
continue;
}
- } catch (PgpGeneralException e) {
- log.add(LogType.MSG_KC_REVOKE_BAD_ERR, indent);
+
+ // first notation? fine then.
+ if (notation == null) {
+ notation = zert;
+ // more notations? at least one is superfluous, then.
+ } else if (notation.getCreationTime().before(zert.getCreationTime())) {
+ log.add(LogType.MSG_KC_NOTATION_DUP, indent);
+ modified = PGPPublicKey.removeCertification(modified, notation);
+ redundantCerts += 1;
+ notation = zert;
+ } else {
+ log.add(LogType.MSG_KC_NOTATION_DUP, indent);
+ modified = PGPPublicKey.removeCertification(modified, zert);
+ redundantCerts += 1;
+ }
+ continue;
+ } else if (cert.isLocal()) {
+ // Remove revocation certs with "local" flag
+ log.add(LogType.MSG_KC_MASTER_BAD_LOCAL, indent);
modified = PGPPublicKey.removeCertification(modified, zert);
badCerts += 1;
continue;
@@ -368,7 +435,17 @@ public class UncachedKeyRing {
}
}
- ArrayList<String> processedUserIds = new ArrayList<String>();
+ // If we have a notation packet, check if there is even any data in it?
+ if (notation != null) {
+ // If there isn't, might as well strip it
+ if (new WrappedSignature(notation).getNotation().isEmpty()) {
+ log.add(LogType.MSG_KC_NOTATION_EMPTY, indent);
+ modified = PGPPublicKey.removeCertification(modified, notation);
+ redundantCerts += 1;
+ }
+ }
+
+ ArrayList<String> processedUserIds = new ArrayList<>();
for (byte[] rawUserId : new IterableIterator<byte[]>(masterKey.getRawUserIDs())) {
String userId = Utf8Util.fromUTF8ByteArrayReplaceBadEncoding(rawUserId);
@@ -393,7 +470,7 @@ public class UncachedKeyRing {
@SuppressWarnings("unchecked")
Iterator<PGPSignature> signaturesIt = masterKey.getSignaturesForID(rawUserId);
if (signaturesIt != null) {
- for (PGPSignature zert : new IterableIterator<PGPSignature>(signaturesIt)) {
+ for (PGPSignature zert : new IterableIterator<>(signaturesIt)) {
WrappedSignature cert = new WrappedSignature(zert);
long certId = cert.getKeyId();
@@ -410,7 +487,7 @@ public class UncachedKeyRing {
continue;
}
- if (cert.getCreationTime().after(now)) {
+ if (cert.getCreationTime().after(nowPlusOneDay)) {
// Creation date in the future? No way!
log.add(LogType.MSG_KC_UID_BAD_TIME, indent);
modified = PGPPublicKey.removeCertification(modified, rawUserId, zert);
@@ -530,6 +607,170 @@ public class UncachedKeyRing {
return null;
}
+ ArrayList<PGPUserAttributeSubpacketVector> processedUserAttributes = new ArrayList<>();
+ for (PGPUserAttributeSubpacketVector userAttribute :
+ new IterableIterator<PGPUserAttributeSubpacketVector>(masterKey.getUserAttributes())) {
+
+ if (userAttribute.getSubpacket(UserAttributeSubpacketTags.IMAGE_ATTRIBUTE) != null) {
+ log.add(LogType.MSG_KC_UAT_JPEG, indent);
+ } else {
+ log.add(LogType.MSG_KC_UAT_UNKNOWN, indent);
+ }
+
+ try {
+ indent += 1;
+
+ // check for duplicate user attributes
+ if (processedUserAttributes.contains(userAttribute)) {
+ log.add(LogType.MSG_KC_UAT_DUP, indent);
+ // strip out the first found user id with this name
+ modified = PGPPublicKey.removeCertification(modified, userAttribute);
+ }
+ processedUserAttributes.add(userAttribute);
+
+ PGPSignature selfCert = null;
+ revocation = null;
+
+ // look through signatures for this specific user id
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSignature> signaturesIt = masterKey.getSignaturesForUserAttribute(userAttribute);
+ if (signaturesIt != null) {
+ for (PGPSignature zert : new IterableIterator<>(signaturesIt)) {
+ WrappedSignature cert = new WrappedSignature(zert);
+ long certId = cert.getKeyId();
+
+ int type = zert.getSignatureType();
+ if (type != PGPSignature.DEFAULT_CERTIFICATION
+ && type != PGPSignature.NO_CERTIFICATION
+ && type != PGPSignature.CASUAL_CERTIFICATION
+ && type != PGPSignature.POSITIVE_CERTIFICATION
+ && type != PGPSignature.CERTIFICATION_REVOCATION) {
+ log.add(LogType.MSG_KC_UAT_BAD_TYPE,
+ indent, "0x" + Integer.toString(zert.getSignatureType(), 16));
+ modified = PGPPublicKey.removeCertification(modified, userAttribute, zert);
+ badCerts += 1;
+ continue;
+ }
+
+ if (cert.getCreationTime().after(nowPlusOneDay)) {
+ // Creation date in the future? No way!
+ log.add(LogType.MSG_KC_UAT_BAD_TIME, indent);
+ modified = PGPPublicKey.removeCertification(modified, userAttribute, zert);
+ badCerts += 1;
+ continue;
+ }
+
+ if (cert.isLocal()) {
+ // Creation date in the future? No way!
+ log.add(LogType.MSG_KC_UAT_BAD_LOCAL, indent);
+ modified = PGPPublicKey.removeCertification(modified, userAttribute, zert);
+ badCerts += 1;
+ continue;
+ }
+
+ // If this is a foreign signature, ...
+ if (certId != masterKeyId) {
+ // never mind any further for public keys, but remove them from secret ones
+ if (isSecret()) {
+ log.add(LogType.MSG_KC_UAT_FOREIGN,
+ indent, KeyFormattingUtils.convertKeyIdToHex(certId));
+ modified = PGPPublicKey.removeCertification(modified, userAttribute, zert);
+ badCerts += 1;
+ }
+ continue;
+ }
+
+ // Otherwise, first make sure it checks out
+ try {
+ cert.init(masterKey);
+ if (!cert.verifySignature(masterKey, userAttribute)) {
+ log.add(LogType.MSG_KC_UAT_BAD,
+ indent);
+ modified = PGPPublicKey.removeCertification(modified, userAttribute, zert);
+ badCerts += 1;
+ continue;
+ }
+ } catch (PgpGeneralException e) {
+ log.add(LogType.MSG_KC_UAT_BAD_ERR,
+ indent);
+ modified = PGPPublicKey.removeCertification(modified, userAttribute, zert);
+ badCerts += 1;
+ continue;
+ }
+
+ switch (type) {
+ case PGPSignature.DEFAULT_CERTIFICATION:
+ case PGPSignature.NO_CERTIFICATION:
+ case PGPSignature.CASUAL_CERTIFICATION:
+ case PGPSignature.POSITIVE_CERTIFICATION:
+ if (selfCert == null) {
+ selfCert = zert;
+ } else if (selfCert.getCreationTime().before(cert.getCreationTime())) {
+ log.add(LogType.MSG_KC_UAT_CERT_DUP,
+ indent);
+ modified = PGPPublicKey.removeCertification(modified, userAttribute, selfCert);
+ redundantCerts += 1;
+ selfCert = zert;
+ } else {
+ log.add(LogType.MSG_KC_UAT_CERT_DUP,
+ indent);
+ modified = PGPPublicKey.removeCertification(modified, userAttribute, zert);
+ redundantCerts += 1;
+ }
+ // If there is a revocation certificate, and it's older than this, drop it
+ if (revocation != null
+ && revocation.getCreationTime().before(selfCert.getCreationTime())) {
+ log.add(LogType.MSG_KC_UAT_REVOKE_OLD,
+ indent);
+ modified = PGPPublicKey.removeCertification(modified, userAttribute, revocation);
+ revocation = null;
+ redundantCerts += 1;
+ }
+ break;
+
+ case PGPSignature.CERTIFICATION_REVOCATION:
+ // If this is older than the (latest) self cert, drop it
+ if (selfCert != null && selfCert.getCreationTime().after(zert.getCreationTime())) {
+ log.add(LogType.MSG_KC_UAT_REVOKE_OLD,
+ indent);
+ modified = PGPPublicKey.removeCertification(modified, userAttribute, zert);
+ redundantCerts += 1;
+ continue;
+ }
+ // first revocation? remember it.
+ if (revocation == null) {
+ revocation = zert;
+ // more revocations? at least one is superfluous, then.
+ } else if (revocation.getCreationTime().before(cert.getCreationTime())) {
+ log.add(LogType.MSG_KC_UAT_REVOKE_DUP,
+ indent);
+ modified = PGPPublicKey.removeCertification(modified, userAttribute, revocation);
+ redundantCerts += 1;
+ revocation = zert;
+ } else {
+ log.add(LogType.MSG_KC_UAT_REVOKE_DUP,
+ indent);
+ modified = PGPPublicKey.removeCertification(modified, userAttribute, zert);
+ redundantCerts += 1;
+ }
+ break;
+ }
+ }
+ }
+
+ // If no valid certificate (if only a revocation) remains, drop it
+ if (selfCert == null && revocation == null) {
+ log.add(LogType.MSG_KC_UAT_REMOVE,
+ indent);
+ modified = PGPPublicKey.removeCertification(modified, userAttribute);
+ }
+
+ } finally {
+ indent -= 1;
+ }
+ }
+
+
// Replace modified key in the keyring
ring = replacePublicKey(ring, modified);
indent -= 1;
@@ -537,7 +778,7 @@ public class UncachedKeyRing {
}
// Keep track of ids we encountered so far
- Set<Long> knownIds = new HashSet<Long>();
+ Set<Long> knownIds = new HashSet<>();
// Process all keys
for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(ring.getPublicKeys())) {
@@ -592,7 +833,7 @@ public class UncachedKeyRing {
continue;
}
- if (cert.getCreationTime().after(now)) {
+ if (cert.getCreationTime().after(nowPlusOneDay)) {
// Creation date in the future? No way!
log.add(LogType.MSG_KC_SUB_BAD_TIME, indent);
badCerts += 1;
@@ -777,8 +1018,8 @@ public class UncachedKeyRing {
/** This operation merges information from a different keyring, returning a combined
* UncachedKeyRing.
*
- * The combined keyring contains the subkeys and user ids of both input keyrings, but it does
- * not necessarily have the canonicalized property.
+ * The combined keyring contains the subkeys, user ids and user attributes of both input
+ * keyrings, but it does not necessarily have the canonicalized property.
*
* @param other The UncachedKeyRing to merge. Must not be empty, and of the same masterKeyId
* @return A consolidated UncachedKeyRing with the data of both input keyrings. Same type as
@@ -801,7 +1042,7 @@ public class UncachedKeyRing {
}
// remember which certs we already added. this is cheaper than semantic deduplication
- Set<byte[]> certs = new TreeSet<byte[]>(new Comparator<byte[]>() {
+ Set<byte[]> certs = new TreeSet<>(new Comparator<byte[]>() {
public int compare(byte[] left, byte[] right) {
// check for length equality
if (left.length != right.length) {
@@ -883,7 +1124,7 @@ public class UncachedKeyRing {
if (signaturesIt == null) {
continue;
}
- for (PGPSignature cert : new IterableIterator<PGPSignature>(signaturesIt)) {
+ for (PGPSignature cert : new IterableIterator<>(signaturesIt)) {
// Don't merge foreign stuff into secret keys
if (cert.getKeyID() != masterKeyId && isSecret()) {
continue;
@@ -898,6 +1139,32 @@ public class UncachedKeyRing {
modified = PGPPublicKey.addCertification(modified, rawUserId, cert);
}
}
+
+ // Copy over all user attribute certificates
+ for (PGPUserAttributeSubpacketVector vector :
+ new IterableIterator<PGPUserAttributeSubpacketVector>(key.getUserAttributes())) {
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSignature> signaturesIt = key.getSignaturesForUserAttribute(vector);
+ // no signatures for this user attribute attribute, skip it
+ if (signaturesIt == null) {
+ continue;
+ }
+ for (PGPSignature cert : new IterableIterator<>(signaturesIt)) {
+ // Don't merge foreign stuff into secret keys
+ if (cert.getKeyID() != masterKeyId && isSecret()) {
+ continue;
+ }
+ byte[] encoded = cert.getEncoded();
+ // Known cert, skip it
+ if (certs.contains(encoded)) {
+ continue;
+ }
+ newCerts += 1;
+ certs.add(encoded);
+ modified = PGPPublicKey.addCertification(modified, vector, cert);
+ }
+ }
+
// If anything changed, save the updated (sub)key
if (modified != resultKey) {
result = replacePublicKey(result, modified);