From 4cfdcca2e015e53f2aea89b225324514b62e927e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Sep 2023 14:42:44 +0200 Subject: [PATCH] Kotlin conversion: MessageMetadata --- .../MessageMetadata.java | 869 ------------------ .../MessageMetadata.kt | 528 +++++++++++ .../OpenPgpMessageInputStream.kt | 21 +- .../SignatureVerification.kt | 13 +- .../MessageMetadataTest.java | 6 +- 5 files changed, 549 insertions(+), 888 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java deleted file mode 100644 index 1f7a5b03..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ /dev/null @@ -1,869 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.util.ArrayList; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.authentication.CertificateAuthenticity; -import org.pgpainless.authentication.CertificateAuthority; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.util.SessionKey; - -/** - * View for extracting metadata about a {@link Message}. - */ -public class MessageMetadata { - - protected Message message; - - public MessageMetadata(@Nonnull Message message) { - this.message = message; - } - - public boolean isUsingCleartextSignatureFramework() { - return message.isCleartextSigned(); - } - - public boolean isEncrypted() { - SymmetricKeyAlgorithm algorithm = getEncryptionAlgorithm(); - return algorithm != null && algorithm != SymmetricKeyAlgorithm.NULL; - } - - public boolean isEncryptedFor(@Nonnull PGPKeyRing keys) { - Iterator encryptionLayers = getEncryptionLayers(); - while (encryptionLayers.hasNext()) { - EncryptedData encryptedData = encryptionLayers.next(); - for (long recipient : encryptedData.getRecipients()) { - PGPPublicKey key = keys.getPublicKey(recipient); - if (key != null) { - return true; - } - } - } - return false; - } - - /** - * Return true, if the message was signed by a certificate for which we can authenticate a binding to the given userId. - * - * @param userId userId - * @param email if true, treat the user-id as an email address and match all userIDs containing this address - * @param certificateAuthority certificate authority - * @return true, if we can authenticate a binding for a signing key with sufficient evidence - */ - public boolean isAuthenticatablySignedBy(String userId, boolean email, CertificateAuthority certificateAuthority) { - return isAuthenticatablySignedBy(userId, email, certificateAuthority, 120); - } - - /** - * Return true, if the message was verifiably signed by a certificate for which we can authenticate a binding to the given userId. - * - * @param userId userId - * @param email if true, treat the user-id as an email address and match all userIDs containing this address - * @param certificateAuthority certificate authority - * @param targetAmount target trust amount - * @return true, if we can authenticate a binding for a signing key with sufficient evidence - */ - public boolean isAuthenticatablySignedBy(String userId, boolean email, CertificateAuthority certificateAuthority, int targetAmount) { - for (SignatureVerification verification : getVerifiedSignatures()) { - CertificateAuthenticity authenticity = certificateAuthority.authenticateBinding( - verification.getSigningKey().getFingerprint(), userId, email, - verification.getSignature().getCreationTime(), targetAmount); - if (authenticity.isAuthenticated()) { - return true; - } - } - return false; - } - - /** - * Return a list containing all recipient keyIDs. - * - * @return list of recipients - */ - public List getRecipientKeyIds() { - List keyIds = new ArrayList<>(); - Iterator encLayers = getEncryptionLayers(); - while (encLayers.hasNext()) { - EncryptedData layer = encLayers.next(); - keyIds.addAll(layer.getRecipients()); - } - return keyIds; - } - - public @Nonnull Iterator getEncryptionLayers() { - return new LayerIterator(message) { - @Override - public boolean matches(Packet layer) { - return layer instanceof EncryptedData; - } - - @Override - public EncryptedData getProperty(Layer last) { - return (EncryptedData) last; - } - }; - } - - /** - * Return the {@link SymmetricKeyAlgorithm} of the outermost encrypted data packet, or null if message is - * unencrypted. - * - * @return encryption algorithm - */ - public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() { - return firstOrNull(getEncryptionAlgorithms()); - } - - /** - * Return an {@link Iterator} of {@link SymmetricKeyAlgorithm SymmetricKeyAlgorithms} encountered in the message. - * The first item returned by the iterator is the algorithm of the outermost encrypted data packet, the next item - * that of the next nested encrypted data packet and so on. - * The iterator might also be empty, in case of an unencrypted message. - * - * @return iterator of symmetric encryption algorithms - */ - public @Nonnull Iterator getEncryptionAlgorithms() { - return map(getEncryptionLayers(), encryptedData -> encryptedData.algorithm); - } - - public @Nonnull Iterator getCompressionLayers() { - return new LayerIterator(message) { - @Override - boolean matches(Packet layer) { - return layer instanceof CompressedData; - } - - @Override - CompressedData getProperty(Layer last) { - return (CompressedData) last; - } - }; - } - - /** - * Return the {@link CompressionAlgorithm} of the outermost compressed data packet, or null, if the message - * does not contain any compressed data packets. - * - * @return compression algorithm - */ - public @Nullable CompressionAlgorithm getCompressionAlgorithm() { - return firstOrNull(getCompressionAlgorithms()); - } - - /** - * Return an {@link Iterator} of {@link CompressionAlgorithm CompressionAlgorithms} encountered in the message. - * The first item returned by the iterator is the algorithm of the outermost compressed data packet, the next - * item that of the next nested compressed data packet and so on. - * The iterator might also be empty, in case of a message without any compressed data packets. - * - * @return iterator of compression algorithms - */ - public @Nonnull Iterator getCompressionAlgorithms() { - return map(getCompressionLayers(), compressionLayer -> compressionLayer.algorithm); - } - - /** - * Return the {@link SessionKey} of the outermost encrypted data packet. - * If the message was unencrypted, this method returns
null
. - * - * @return session key of the message - */ - public @Nullable SessionKey getSessionKey() { - return firstOrNull(getSessionKeys()); - } - - /** - * Return an {@link Iterator} of {@link SessionKey SessionKeys} for all encrypted data packets in the message. - * The first item returned by the iterator is the session key of the outermost encrypted data packet, - * the next item that of the next nested encrypted data packet and so on. - * The iterator might also be empty, in case of an unencrypted message. - * - * @return iterator of session keys - */ - public @Nonnull Iterator getSessionKeys() { - return map(getEncryptionLayers(), encryptedData -> encryptedData.sessionKey); - } - - public boolean isVerifiedSignedBy(@Nonnull PGPKeyRing keys) { - return isVerifiedInlineSignedBy(keys) || isVerifiedDetachedSignedBy(keys); - } - - /** - * Return true, if the message was verifiable signed by a certificate that either has the given fingerprint - * as primary key, or as the signing subkey. - * - * @param fingerprint fingerprint - * @return true if message was signed by a cert identified by the given fingerprint - */ - public boolean isVerifiedSignedBy(@Nonnull OpenPgpFingerprint fingerprint) { - List verifications = getVerifiedSignatures(); - for (SignatureVerification verification : verifications) { - if (verification.getSigningKey() == null) { - continue; - } - - if (fingerprint.equals(verification.getSigningKey().getPrimaryKeyFingerprint()) || - fingerprint.equals(verification.getSigningKey().getSubkeyFingerprint())) { - return true; - } - } - return false; - } - - public List getVerifiedSignatures() { - List allVerifiedSignatures = getVerifiedInlineSignatures(); - allVerifiedSignatures.addAll(getVerifiedDetachedSignatures()); - return allVerifiedSignatures; - } - - public boolean isVerifiedDetachedSignedBy(@Nonnull PGPKeyRing keys) { - return containsSignatureBy(getVerifiedDetachedSignatures(), keys); - } - - /** - * Return a list of all verified detached signatures. - * This list contains all acceptable, correct detached signatures. - * - * @return verified detached signatures - */ - public @Nonnull List getVerifiedDetachedSignatures() { - return message.getVerifiedDetachedSignatures(); - } - - /** - * Return a list of all rejected detached signatures. - * - * @return rejected detached signatures - */ - public @Nonnull List getRejectedDetachedSignatures() { - return message.getRejectedDetachedSignatures(); - } - - /** - * Return a list of all rejected signatures. - * - * @return rejected signatures - */ - public @Nonnull List getRejectedSignatures() { - List rejected = new ArrayList<>(); - rejected.addAll(getRejectedInlineSignatures()); - rejected.addAll(getRejectedDetachedSignatures()); - return rejected; - } - - public boolean hasRejectedSignatures() { - return !getRejectedSignatures().isEmpty(); - } - - /** - * Return true, if the message contains any (verified or rejected) signature. - * @return true if message has signature - */ - public boolean hasSignature() { - return isVerifiedSigned() || hasRejectedSignatures(); - } - - public boolean isVerifiedInlineSignedBy(@Nonnull PGPKeyRing keys) { - return containsSignatureBy(getVerifiedInlineSignatures(), keys); - } - - /** - * Return a list of all verified inline-signatures. - * This list contains all acceptable, correct signatures that were part of the message itself. - * - * @return verified inline signatures - */ - public @Nonnull List getVerifiedInlineSignatures() { - List verifications = new ArrayList<>(); - Iterator> verificationsByLayer = getVerifiedInlineSignaturesByLayer(); - while (verificationsByLayer.hasNext()) { - verifications.addAll(verificationsByLayer.next()); - } - return verifications; - } - - /** - * Return an {@link Iterator} of {@link List Lists} of verified inline-signatures of the message. - * Since signatures might occur in different layers within a message, this method can be used to gain more detailed - * insights into what signatures were encountered at what layers of the message structure. - * Each item of the {@link Iterator} represents a layer of the message and contains only signatures from - * this layer. - * An empty list means no (or no acceptable) signatures were encountered in that layer. - * - * @return iterator of lists of signatures by-layer. - */ - public @Nonnull Iterator> getVerifiedInlineSignaturesByLayer() { - return new LayerIterator>(message) { - @Override - boolean matches(Packet layer) { - return layer instanceof Layer; - } - - @Override - List getProperty(Layer last) { - List list = new ArrayList<>(); - list.addAll(last.getVerifiedOnePassSignatures()); - list.addAll(last.getVerifiedPrependedSignatures()); - return list; - } - }; - } - - /** - * Return a list of all rejected inline-signatures of the message. - * - * @return list of rejected inline-signatures - */ - public @Nonnull List getRejectedInlineSignatures() { - List rejected = new ArrayList<>(); - Iterator> rejectedByLayer = getRejectedInlineSignaturesByLayer(); - while (rejectedByLayer.hasNext()) { - rejected.addAll(rejectedByLayer.next()); - } - return rejected; - } - - /** - * Similar to {@link #getVerifiedInlineSignaturesByLayer()}, this method returns all rejected inline-signatures - * of the message, but organized by layer. - * - * @return rejected inline-signatures by-layer - */ - public @Nonnull Iterator> getRejectedInlineSignaturesByLayer() { - return new LayerIterator>(message) { - @Override - boolean matches(Packet layer) { - return layer instanceof Layer; - } - - @Override - List getProperty(Layer last) { - List list = new ArrayList<>(); - list.addAll(last.getRejectedOnePassSignatures()); - list.addAll(last.getRejectedPrependedSignatures()); - return list; - } - }; - } - - private static boolean containsSignatureBy(@Nonnull List verifications, - @Nonnull PGPKeyRing keys) { - for (SignatureVerification verification : verifications) { - SubkeyIdentifier issuer = verification.getSigningKey(); - if (issuer == null) { - // No issuer, shouldn't happen, but better be safe and skip... - continue; - } - - if (keys.getPublicKey().getKeyID() != issuer.getPrimaryKeyId()) { - // Wrong cert - continue; - } - - if (keys.getPublicKey(issuer.getSubkeyId()) != null) { - // Matching cert and signing key - return true; - } - } - return false; - } - - /** - * Return the value of the literal data packet's filename field. - * This value can be used to store a decrypted file under its original filename, - * but since this field is not necessarily part of the signed data of a message, usage of this field is - * discouraged. - * - * @return filename - * @see RFC4880 §5.9. Literal Data Packet - */ - public @Nullable String getFilename() { - LiteralData literalData = findLiteralData(); - if (literalData == null) { - return null; - } - return literalData.getFileName(); - } - - /** - * Returns true, if the filename of the literal data packet indicates that the data is intended for your eyes only. - * - * @return isForYourEyesOnly - */ - public boolean isForYourEyesOnly() { - return PGPLiteralData.CONSOLE.equals(getFilename()); - } - - /** - * Return the value of the literal data packets modification date field. - * This value can be used to restore the modification date of a decrypted file, - * but since this field is not necessarily part of the signed data, its use is discouraged. - * - * @return modification date - * @see RFC4880 §5.9. Literal Data Packet - */ - public @Nullable Date getModificationDate() { - LiteralData literalData = findLiteralData(); - if (literalData == null) { - return null; - } - return literalData.getModificationDate(); - } - - /** - * Return the value of the format field of the literal data packet. - * This value indicates what format (text, binary data, ...) the data has. - * Since this field is not necessarily part of the signed data of a message, its usage is discouraged. - * - * @return format - * @see RFC4880 §5.9. Literal Data Packet - */ - public @Nullable StreamEncoding getLiteralDataEncoding() { - LiteralData literalData = findLiteralData(); - if (literalData == null) { - return null; - } - return literalData.getFormat(); - } - - /** - * Find the {@link LiteralData} layer of an OpenPGP message. - * Usually, every message has a literal data packet, but for malformed messages this method might still - * return
null
. - * - * @return literal data - */ - private @Nullable LiteralData findLiteralData() { - Nested nested = message.getChild(); - if (nested == null) { - return null; - } - - while (nested != null && nested.hasNestedChild()) { - Layer layer = (Layer) nested; - nested = layer.getChild(); - } - return (LiteralData) nested; - } - - /** - * Return the {@link SubkeyIdentifier} of the decryption key that was used to decrypt the outermost encryption - * layer. - * If the message was unencrypted, this might return
null
. - * - * @return decryption key - */ - public SubkeyIdentifier getDecryptionKey() { - return firstOrNull(map(getEncryptionLayers(), encryptedData -> encryptedData.decryptionKey)); - } - - public boolean isVerifiedSigned() { - return !getVerifiedSignatures().isEmpty(); - } - - public interface Packet { - - } - public abstract static class Layer implements Packet { - public static final int MAX_LAYER_DEPTH = 16; - protected final int depth; - protected final List verifiedDetachedSignatures = new ArrayList<>(); - protected final List rejectedDetachedSignatures = new ArrayList<>(); - protected final List verifiedOnePassSignatures = new ArrayList<>(); - protected final List rejectedOnePassSignatures = new ArrayList<>(); - protected final List verifiedPrependedSignatures = new ArrayList<>(); - protected final List rejectedPrependedSignatures = new ArrayList<>(); - protected Nested child; - - public Layer(int depth) { - this.depth = depth; - if (depth > MAX_LAYER_DEPTH) { - throw new MalformedOpenPgpMessageException("Maximum packet nesting depth (" + MAX_LAYER_DEPTH + ") exceeded."); - } - } - - /** - * Return the nested child element of this layer. - * Might return
null
, if this layer does not have a child element - * (e.g. if this is a {@link LiteralData} packet). - * - * @return child element - */ - public @Nullable Nested getChild() { - return child; - } - - /** - * Set the nested child element for this layer. - * - * @param child child element - */ - void setChild(Nested child) { - this.child = child; - } - - /** - * Return a list of all verified detached signatures of this layer. - * - * @return all verified detached signatures of this layer - */ - public List getVerifiedDetachedSignatures() { - return new ArrayList<>(verifiedDetachedSignatures); - } - - /** - * Return a list of all rejected detached signatures of this layer. - * - * @return all rejected detached signatures of this layer - */ - public List getRejectedDetachedSignatures() { - return new ArrayList<>(rejectedDetachedSignatures); - } - - /** - * Add a verified detached signature for this layer. - * - * @param signatureVerification verified detached signature - */ - void addVerifiedDetachedSignature(SignatureVerification signatureVerification) { - verifiedDetachedSignatures.add(signatureVerification); - } - - /** - * Add a rejected detached signature for this layer. - * - * @param failure rejected detached signature - */ - void addRejectedDetachedSignature(SignatureVerification.Failure failure) { - rejectedDetachedSignatures.add(failure); - } - - /** - * Return a list of all verified one-pass-signatures of this layer. - * - * @return all verified one-pass-signatures of this layer - */ - public List getVerifiedOnePassSignatures() { - return new ArrayList<>(verifiedOnePassSignatures); - } - - /** - * Return a list of all rejected one-pass-signatures of this layer. - * - * @return all rejected one-pass-signatures of this layer - */ - public List getRejectedOnePassSignatures() { - return new ArrayList<>(rejectedOnePassSignatures); - } - - /** - * Add a verified one-pass-signature for this layer. - * - * @param verifiedOnePassSignature verified one-pass-signature for this layer - */ - void addVerifiedOnePassSignature(SignatureVerification verifiedOnePassSignature) { - this.verifiedOnePassSignatures.add(verifiedOnePassSignature); - } - - /** - * Add a rejected one-pass-signature for this layer. - * - * @param rejected rejected one-pass-signature for this layer - */ - void addRejectedOnePassSignature(SignatureVerification.Failure rejected) { - this.rejectedOnePassSignatures.add(rejected); - } - - /** - * Return a list of all verified prepended signatures of this layer. - * - * @return all verified prepended signatures of this layer - */ - public List getVerifiedPrependedSignatures() { - return new ArrayList<>(verifiedPrependedSignatures); - } - - /** - * Return a list of all rejected prepended signatures of this layer. - * - * @return all rejected prepended signatures of this layer - */ - public List getRejectedPrependedSignatures() { - return new ArrayList<>(rejectedPrependedSignatures); - } - - /** - * Add a verified prepended signature for this layer. - * - * @param verified verified prepended signature - */ - void addVerifiedPrependedSignature(SignatureVerification verified) { - this.verifiedPrependedSignatures.add(verified); - } - - /** - * Add a rejected prepended signature for this layer. - * - * @param rejected rejected prepended signature - */ - void addRejectedPrependedSignature(SignatureVerification.Failure rejected) { - this.rejectedPrependedSignatures.add(rejected); - } - - } - - public interface Nested extends Packet { - boolean hasNestedChild(); - } - - public static class Message extends Layer { - - protected boolean cleartextSigned; - - public Message() { - super(0); - } - - /** - * Returns true, is the message is a signed message using the cleartext signature framework. - * - * @return
true
if message is cleartext-signed,
false
otherwise - * @see RFC4880 §7. Cleartext Signature Framework - */ - public boolean isCleartextSigned() { - return cleartextSigned; - } - - } - - public static class LiteralData implements Nested { - protected String fileName; - protected Date modificationDate; - protected StreamEncoding format; - - public LiteralData() { - this("", new Date(0L), StreamEncoding.BINARY); - } - - public LiteralData(@Nonnull String fileName, - @Nonnull Date modificationDate, - @Nonnull StreamEncoding format) { - this.fileName = fileName; - this.modificationDate = modificationDate; - this.format = format; - } - - /** - * Return the value of the filename field. - * An empty String
""
indicates no filename. - * - * @return filename - */ - public @Nonnull String getFileName() { - return fileName; - } - - /** - * Return the value of the modification date field. - * A special date
{@code new Date(0L)}
indicates no modification date. - * - * @return modification date - */ - public @Nonnull Date getModificationDate() { - return modificationDate; - } - - /** - * Return the value of the format field. - * - * @return format - */ - public @Nonnull StreamEncoding getFormat() { - return format; - } - - @Override - public boolean hasNestedChild() { - // A literal data packet MUST NOT have a child element, as its content is the plaintext - return false; - } - } - - public static class CompressedData extends Layer implements Nested { - protected final CompressionAlgorithm algorithm; - - public CompressedData(@Nonnull CompressionAlgorithm zip, int depth) { - super(depth); - this.algorithm = zip; - } - - /** - * Return the {@link CompressionAlgorithm} used to compress the packet. - * @return compression algorithm - */ - public @Nonnull CompressionAlgorithm getAlgorithm() { - return algorithm; - } - - @Override - public boolean hasNestedChild() { - // A compressed data packet MUST have a child element - return true; - } - } - - public static class EncryptedData extends Layer implements Nested { - protected final SymmetricKeyAlgorithm algorithm; - protected SubkeyIdentifier decryptionKey; - protected SessionKey sessionKey; - protected List recipients; - - public EncryptedData(@Nonnull SymmetricKeyAlgorithm algorithm, int depth) { - super(depth); - this.algorithm = algorithm; - } - - /** - * Return the {@link SymmetricKeyAlgorithm} used to encrypt the packet. - * @return symmetric encryption algorithm - */ - public @Nonnull SymmetricKeyAlgorithm getAlgorithm() { - return algorithm; - } - - /** - * Return the {@link SessionKey} used to decrypt the packet. - * - * @return session key - */ - public @Nonnull SessionKey getSessionKey() { - return sessionKey; - } - - /** - * Return a list of all recipient key ids to which the packet was encrypted for. - * - * @return recipients - */ - public @Nonnull List getRecipients() { - if (recipients == null) { - return new ArrayList<>(); - } - return new ArrayList<>(recipients); - } - - @Override - public boolean hasNestedChild() { - // An encrypted data packet MUST have a child element - return true; - } - } - - - private abstract static class LayerIterator implements Iterator { - private Nested current; - Layer last = null; - Message parent; - - LayerIterator(@Nonnull Message message) { - super(); - this.parent = message; - this.current = message.getChild(); - if (matches(current)) { - last = (Layer) current; - } - } - - @Override - public boolean hasNext() { - if (parent != null && matches(parent)) { - return true; - } - if (last == null) { - findNext(); - } - return last != null; - } - - @Override - public O next() { - if (parent != null && matches(parent)) { - O property = getProperty(parent); - parent = null; - return property; - } - if (last == null) { - findNext(); - } - if (last != null) { - O property = getProperty(last); - last = null; - return property; - } - throw new NoSuchElementException(); - } - - private void findNext() { - while (current != null && current instanceof Layer) { - current = ((Layer) current).getChild(); - if (matches(current)) { - last = (Layer) current; - break; - } - } - } - - abstract boolean matches(Packet layer); - - abstract O getProperty(Layer last); - } - - private static Iterator map(Iterator from, Function mapping) { - return new Iterator() { - @Override - public boolean hasNext() { - return from.hasNext(); - } - - @Override - public B next() { - return mapping.apply(from.next()); - } - }; - } - - public interface Function { - B apply(A item); - } - - private static @Nullable A firstOrNull(Iterator iterator) { - if (iterator.hasNext()) { - return iterator.next(); - } - return null; - } - - private static @Nonnull A firstOr(Iterator iterator, A item) { - if (iterator.hasNext()) { - return iterator.next(); - } - return item; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt new file mode 100644 index 00000000..c4f31f4c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt @@ -0,0 +1,528 @@ +package org.pgpainless.decryption_verification + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPLiteralData +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.authentication.CertificateAuthority +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.util.SessionKey +import java.util.* +import javax.annotation.Nonnull + +/** + * View for extracting metadata about a [Message]. + */ +class MessageMetadata( + val message: Message +) { + + // ################################################################################################################ + // ### Encryption ### + // ################################################################################################################ + + /** + * The [SymmetricKeyAlgorithm] of the outermost encrypted data packet, or null if message is unencrypted. + */ + val encryptionAlgorithm: SymmetricKeyAlgorithm? + get() = encryptionAlgorithms.let { + if (it.hasNext()) it.next() else null + } + + /** + * [Iterator] of each [SymmetricKeyAlgorithm] encountered in the message. + * The first item returned by the iterator is the algorithm of the outermost encrypted data packet, the next item + * that of the next nested encrypted data packet and so on. + * The iterator might also be empty, in case of an unencrypted message. + */ + val encryptionAlgorithms: Iterator + get() = encryptionLayers.asSequence().map { it.algorithm }.iterator() + + val isEncrypted: Boolean + get() = if (encryptionAlgorithm == null) false else encryptionAlgorithm != SymmetricKeyAlgorithm.NULL + + fun isEncryptedFor(keys: PGPKeyRing): Boolean { + return encryptionLayers.asSequence().any { + it.recipients.any { keyId -> + keys.getPublicKey(keyId) != null + } + } + } + + /** + * [SessionKey] of the outermost encrypted data packet. + * If the message was unencrypted, this method returns `null`. + */ + val sessionKey: SessionKey? + get() = sessionKeys.asSequence().firstOrNull() + + /** + * [Iterator] of each [SessionKey] for all encrypted data packets in the message. + * The first item returned by the iterator is the session key of the outermost encrypted data packet, + * the next item that of the next nested encrypted data packet and so on. + * The iterator might also be empty, in case of an unencrypted message. + */ + val sessionKeys: Iterator + get() = encryptionLayers.asSequence().mapNotNull { it.sessionKey }.iterator() + + /** + * [SubkeyIdentifier] of the decryption key that was used to decrypt the outermost encryption + * layer. + * If the message was unencrypted or was decrypted using a passphrase, this field might be `null`. + */ + val decryptionKey: SubkeyIdentifier? + get() = encryptionLayers.asSequence() + .mapNotNull { it.decryptionKey } + .firstOrNull() + + /** + * List containing all recipient keyIDs. + */ + val recipientKeyIds: List + get() = encryptionLayers.asSequence() + .map { it.recipients.toMutableList() } + .reduce { all, keyIds -> all.addAll(keyIds); all } + .toList() + + val encryptionLayers: Iterator + get() = object : LayerIterator(message) { + override fun matches(layer: Packet) = layer is EncryptedData + override fun getProperty(last: Layer) = last as EncryptedData + } + + // ################################################################################################################ + // ### Compression ### + // ################################################################################################################ + + /** + * [CompressionAlgorithm] of the outermost compressed data packet, or null, if the message + * does not contain any compressed data packets. + */ + val compressionAlgorithm: CompressionAlgorithm? = compressionAlgorithms.asSequence().firstOrNull() + + /** + * [Iterator] of each [CompressionAlgorithm] encountered in the message. + * The first item returned by the iterator is the algorithm of the outermost compressed data packet, the next + * item that of the next nested compressed data packet and so on. + * The iterator might also be empty, in case of a message without any compressed data packets. + */ + val compressionAlgorithms: Iterator + get() = compressionLayers.asSequence().map { it.algorithm }.iterator() + + val compressionLayers: Iterator + get() = object : LayerIterator(message) { + override fun matches(layer: Packet) = layer is CompressedData + override fun getProperty(last: Layer) = last as CompressedData + } + + // ################################################################################################################ + // ### Signatures ### + // ################################################################################################################ + + val isUsingCleartextSignatureFramework: Boolean + get() = message.cleartextSigned + + val verifiedSignatures: List + get() = verifiedInlineSignatures.plus(verifiedDetachedSignatures) + + /** + * List of all rejected signatures. + */ + val rejectedSignatures: List + get() = mutableListOf() + .plus(rejectedInlineSignatures) + .plus(rejectedDetachedSignatures) + .toList() + + /** + * List of all verified inline-signatures. + * This list contains all acceptable, correct signatures that were part of the message itself. + */ + val verifiedInlineSignatures: List = verifiedInlineSignaturesByLayer + .asSequence() + .map { it.toMutableList() } + .reduce { acc, signatureVerifications -> acc.addAll(signatureVerifications); acc } + .toList() + + /** + * [Iterator] of each [List] of verified inline-signatures of the message, separated by layer. + * Since signatures might occur in different layers within a message, this method can be used to gain more detailed + * insights into what signatures were encountered at what layers of the message structure. + * Each item of the [Iterator] represents a layer of the message and contains only signatures from + * this layer. + * An empty list means no (or no acceptable) signatures were encountered in that layer. + */ + val verifiedInlineSignaturesByLayer: Iterator> + get() = object : LayerIterator>(message) { + override fun matches(layer: Packet) = layer is Layer + + override fun getProperty(last: Layer): List { + return listOf() + .plus(last.verifiedOnePassSignatures) + .plus(last.verifiedPrependedSignatures) + } + + } + + /** + * List of all rejected inline-signatures of the message. + */ + val rejectedInlineSignatures: List = rejectedInlineSignaturesByLayer + .asSequence() + .map { it.toMutableList() } + .reduce { acc, failures -> acc.addAll(failures); acc} + .toList() + + /** + * Similar to [verifiedInlineSignaturesByLayer], this field contains all rejected inline-signatures + * of the message, but organized by layer. + */ + val rejectedInlineSignaturesByLayer: Iterator> + get() = object : LayerIterator>(message) { + override fun matches(layer: Packet) = layer is Layer + + override fun getProperty(last: Layer): List = + mutableListOf() + .plus(last.rejectedOnePassSignatures) + .plus(last.rejectedPrependedSignatures) + } + + /** + * List of all verified detached signatures. + * This list contains all acceptable, correct detached signatures. + */ + val verifiedDetachedSignatures: List = message.verifiedDetachedSignatures + + /** + * List of all rejected detached signatures. + */ + val rejectedDetachedSignatures: List = message.rejectedDetachedSignatures + + /** + * True, if the message contains any (verified or rejected) signature, false if no signatures are present. + */ + val hasSignature: Boolean + get() = isVerifiedSigned() || hasRejectedSignatures() + + fun isVerifiedSigned(): Boolean = verifiedSignatures.isNotEmpty() + + fun hasRejectedSignatures(): Boolean = rejectedSignatures.isNotEmpty() + + /** + * Return true, if the message was signed by a certificate for which we can authenticate a binding to the given userId. + * + * @param userId userId + * @param email if true, treat the user-id as an email address and match all userIDs containing this address + * @param certificateAuthority certificate authority + * @param targetAmount targeted trust amount that needs to be reached by the binding to qualify as authenticated. + * defaults to 120. + * @return true, if we can authenticate a binding for a signing key with sufficient evidence + */ + @JvmOverloads + fun isAuthenticatablySignedBy(userId: String, email: Boolean, certificateAuthority: CertificateAuthority, targetAmount: Int = 120): Boolean { + return verifiedSignatures.any { + certificateAuthority.authenticateBinding( + it.signingKey.fingerprint, userId, email, it.signature.creationTime, targetAmount + ).authenticated + } + } + + /** + * Return rue, if the message was verifiable signed by a certificate that either has the given fingerprint + * as primary key, or as the signing subkey. + * + * @param fingerprint fingerprint + * @return true if message was signed by a cert identified by the given fingerprint + */ + fun isVerifiedSignedBy(fingerprint: OpenPgpFingerprint) = verifiedSignatures.any { + it.signingKey.primaryKeyFingerprint == fingerprint || it.signingKey.subkeyFingerprint == fingerprint + } + + fun isVerifiedSignedBy(keys: PGPKeyRing) = containsSignatureBy(verifiedSignatures, keys) + + fun isVerifiedDetachedSignedBy(fingerprint: OpenPgpFingerprint) = verifiedDetachedSignatures.any { + it.signingKey.primaryKeyFingerprint == fingerprint || it.signingKey.subkeyFingerprint == fingerprint + } + + fun isVerifiedDetachedSignedBy(keys: PGPKeyRing) = containsSignatureBy(verifiedDetachedSignatures, keys) + + fun isVerifiedInlineSignedBy(fingerprint: OpenPgpFingerprint) = verifiedInlineSignatures.any { + it.signingKey.primaryKeyFingerprint == fingerprint || it.signingKey.subkeyFingerprint == fingerprint + } + + fun isVerifiedInlineSignedBy(keys: PGPKeyRing) = containsSignatureBy(verifiedInlineSignatures, keys) + + private fun containsSignatureBy(signatures: List, keys: PGPKeyRing) = + signatures.any { + // Match certificate by primary key id + keys.publicKey.keyID == it.signingKey.primaryKeyId && + // match signing subkey + keys.getPublicKey(it.signingKey.subkeyId) != null + } + + // ################################################################################################################ + // ### Literal Data ### + // ################################################################################################################ + + /** + * Value of the literal data packet's filename field. + * This value can be used to store a decrypted file under its original filename, + * but since this field is not necessarily part of the signed data of a message, usage of this field is + * discouraged. + * + * @see RFC4880 §5.9. Literal Data Packet + */ + val filename: String? = findLiteralData()?.fileName + + /** + * True, if the sender signals an increased degree of confidentiality by setting the filename of the literal + * data packet to a special value that indicates that the data is intended for your eyes only. + */ + @Deprecated("Reliance on this signaling mechanism is discouraged.") + val isForYourEyesOnly: Boolean = PGPLiteralData.CONSOLE == filename + + /** + * Value of the literal data packets modification date field. + * This value can be used to restore the modification date of a decrypted file, + * but since this field is not necessarily part of the signed data, its use is discouraged. + * + * @see RFC4880 §5.9. Literal Data Packet + */ + val modificationDate: Date? = findLiteralData()?.modificationDate + + /** + * Value of the format field of the literal data packet. + * This value indicates what format (text, binary data, ...) the data has. + * Since this field is not necessarily part of the signed data of a message, its usage is discouraged. + * + * @see RFC4880 §5.9. Literal Data Packet + */ + val literalDataEncoding: StreamEncoding? = findLiteralData()?.format + + /** + * Find the [LiteralData] layer of an OpenPGP message. + * This method might return null, for example for a cleartext signed message without OpenPGP packets. + * + * @return literal data + */ + private fun findLiteralData(): LiteralData? { + // If the message is a non-OpenPGP message with a detached signature, or a Cleartext Signed message, + // we might not have a Literal Data packet. + var nested = message.child ?: return null + + while (nested.hasNestedChild()) { + val layer = nested as Layer + nested = checkNotNull(layer.child) { + // Otherwise, we MUST find a Literal Data packet, or else the message is malformed + "Malformed OpenPGP message. Cannot find Literal Data Packet" + } + } + return nested as LiteralData + } + + // ################################################################################################################ + // ### Message Structure ### + // ################################################################################################################ + + interface Packet + + interface Nested : Packet { + fun hasNestedChild(): Boolean + } + + abstract class Layer( + val depth: Int + ) : Packet { + + init { + if (depth > MAX_LAYER_DEPTH) { + throw MalformedOpenPgpMessageException("Maximum packet nesting depth ($MAX_LAYER_DEPTH) exceeded.") + } + } + + val verifiedDetachedSignatures: List = mutableListOf() + val rejectedDetachedSignatures: List = mutableListOf() + val verifiedOnePassSignatures: List = mutableListOf() + val rejectedOnePassSignatures: List = mutableListOf() + val verifiedPrependedSignatures: List = mutableListOf() + val rejectedPrependedSignatures: List = mutableListOf() + + /** + * Nested child element of this layer. + * Might be `null`, if this layer does not have a child element + * (e.g. if this is a [LiteralData] packet). + */ + var child: Nested? = null + + fun addVerifiedDetachedSignature(signature: SignatureVerification) = apply { + (verifiedDetachedSignatures as MutableList).add(signature) + } + + fun addRejectedDetachedSignature(failure: SignatureVerification.Failure) = apply { + (rejectedDetachedSignatures as MutableList).add(failure) + } + + fun addVerifiedOnePassSignature(signature: SignatureVerification) = apply { + (verifiedOnePassSignatures as MutableList).add(signature) + } + + fun addRejectedOnePassSignature(failure: SignatureVerification.Failure) = apply { + (rejectedOnePassSignatures as MutableList).add(failure) + } + + fun addVerifiedPrependedSignature(signature: SignatureVerification) = apply { + (verifiedPrependedSignatures as MutableList).add(signature) + } + + fun addRejectedPrependedSignature(failure: SignatureVerification.Failure) = apply { + (rejectedPrependedSignatures as MutableList).add(failure) + } + + companion object { + const val MAX_LAYER_DEPTH = 16 + } + } + + /** + * Outermost OpenPGP Message structure. + * + * @param cleartextSigned whether the message is using the Cleartext Signature Framework + * + * @see RFC4880 §7. Cleartext Signature Framework + */ + class Message(var cleartextSigned: Boolean = false) : Layer(0) { + fun setCleartextSigned() = apply { cleartextSigned = true } + } + + /** + * Literal Data Packet. + * + * @param fileName value of the filename field. An empty String represents no filename. + * @param modificationDate value of the modification date field. The special value `Date(0)` indicates no + * modification date. + * @param format value of the format field. + */ + class LiteralData( + val fileName: String = "", + val modificationDate: Date = Date(0L), + val format: StreamEncoding = StreamEncoding.BINARY + ) : Nested { + + // A literal data packet MUST NOT have a child element, as its content is the plaintext + override fun hasNestedChild() = false + } + + /** + * Compressed Data Packet. + * + * @param algorithm [CompressionAlgorithm] used to compress the packet. + * @param depth nesting depth at which this packet was encountered. + */ + class CompressedData( + val algorithm: CompressionAlgorithm, + depth: Int) : Layer(depth), Nested { + + // A compressed data packet MUST have a child element + override fun hasNestedChild() = true + } + + /** + * Encrypted Data. + * + * @param algorithm symmetric key algorithm used to encrypt the packet. + * @param depth nesting depth at which this packet was encountered. + */ + class EncryptedData( + val algorithm: SymmetricKeyAlgorithm, + depth: Int + ) : Layer(depth), Nested { + + /** + * [SessionKey] used to decrypt the packet. + */ + var sessionKey: SessionKey? = null + + /** + * List of all recipient key ids to which the packet was encrypted for. + */ + val recipients: List = mutableListOf() + + fun addRecipients(keyIds: List) = apply { + (recipients as MutableList).addAll(keyIds) + } + + /** + * Identifier of the subkey that was used to decrypt the packet (in case of a public key encrypted packet). + */ + var decryptionKey: SubkeyIdentifier? = null + + // An encrypted data packet MUST have a child element + override fun hasNestedChild() = true + + } + + /** + * Iterator that iterates the packet structure from outermost to innermost packet, emitting the results of + * a transformation ([getProperty]) on those packets that match ([matches]) a given criterion. + * + * @param message outermost structure object + */ + private abstract class LayerIterator(@Nonnull message: Message) : Iterator { + private var current: Nested? + var last: Layer? = null + var parent: Message? + + init { + parent = message + current = message.child + current?.let { + if (matches(it)) { + last = current as Layer + } + } + } + + override fun hasNext(): Boolean { + parent?.let { + if (matches(it)) { + return true + } + } + if (last == null) { + findNext() + } + return last != null + } + + override fun next(): O { + parent?.let { + if (matches(it)) { + return getProperty(it).also { parent = null } + } + } + if (last == null) { + findNext() + } + last?.let { + return getProperty(it).also { last = null } + } + throw NoSuchElementException() + } + + private fun findNext() { + while (current != null && current is Layer) { + current = (current as Layer).child + if (current != null && matches(current!!)) { + last = current as Layer + break + } + } + } + + abstract fun matches(layer: Packet): Boolean + abstract fun getProperty(last: Layer): O + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 4b7b1f1f..87b0847a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -156,9 +156,9 @@ class OpenPgpMessageInputStream( val literalData = packetInputStream!!.readLiteralData() // Extract Metadata - layerMetadata.setChild(LiteralData( + layerMetadata.child = LiteralData( literalData.fileName, literalData.modificationTime, - StreamEncoding.requireFromCode(literalData.format))) + StreamEncoding.requireFromCode(literalData.format)) nestedInputStream = literalData.inputStream } @@ -394,7 +394,7 @@ class OpenPgpMessageInputStream( throwIfUnacceptable(sessionKey.algorithm) val encryptedData = EncryptedData(sessionKey.algorithm, layerMetadata.depth + 1) encryptedData.sessionKey = sessionKey - encryptedData.recipients = esks.pkesks.map { it.keyID } + encryptedData.addRecipients(esks.pkesks.map { it.keyID }) LOGGER.debug("Successfully decrypted data with passphrase") val integrityProtected = IntegrityProtectedInputStream(decrypted, skesk, options) nestedInputStream = OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) @@ -421,7 +421,7 @@ class OpenPgpMessageInputStream( layerMetadata.depth + 1) encryptedData.decryptionKey = decryptionKeyId encryptedData.sessionKey = sessionKey - encryptedData.recipients = esks.pkesks.plus(esks.anonPkesks).map { it.keyID } + encryptedData.addRecipients(esks.pkesks.plus(esks.anonPkesks).map { it.keyID }) LOGGER.debug("Successfully decrypted data with key $decryptionKeyId") val integrityProtected = IntegrityProtectedInputStream(decrypted, pkesk, options) nestedInputStream = OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) @@ -522,7 +522,7 @@ class OpenPgpMessageInputStream( private fun collectMetadata() { if (nestedInputStream is OpenPgpMessageInputStream) { val child = nestedInputStream as OpenPgpMessageInputStream - layerMetadata.setChild(child.layerMetadata as Nested) + layerMetadata.child = (child.layerMetadata as Nested) } } @@ -620,8 +620,7 @@ class OpenPgpMessageInputStream( } else { LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") detachedSignaturesWithMissingCert.add(SignatureVerification.Failure( - SignatureVerification(signature, null), - SignatureValidationException("Missing verification key."))) + signature, null, SignatureValidationException("Missing verification key."))) } } @@ -633,8 +632,7 @@ class OpenPgpMessageInputStream( } else { LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") prependedSignaturesWithMissingCert.add(SignatureVerification.Failure( - SignatureVerification(signature, null), - SignatureValidationException("Missing verification key") + signature, null, SignatureValidationException("Missing verification key") )) } } @@ -695,8 +693,7 @@ class OpenPgpMessageInputStream( if (!found) { LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") inbandSignaturesWithMissingCert.add(SignatureVerification.Failure( - SignatureVerification(signature, null), - SignatureValidationException("Missing verification key.") + signature, null, SignatureValidationException("Missing verification key.") )) } } @@ -890,7 +887,7 @@ class OpenPgpMessageInputStream( return if (openPgpIn.isAsciiArmored) { val armorIn = ArmoredInputStreamFactory.get(openPgpIn) if (armorIn.isClearText) { - (metadata as Message).cleartextSigned = true + (metadata as Message).setCleartextSigned() OpenPgpMessageInputStream(Type.cleartext_signed, armorIn, options, metadata, policy) } else { // Simply consume dearmored OpenPGP message diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt index a188f6a5..8d229fb2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt @@ -22,12 +22,12 @@ import org.pgpainless.signature.SignatureUtils */ data class SignatureVerification( val signature: PGPSignature, - val signingKey: SubkeyIdentifier? + val signingKey: SubkeyIdentifier ) { override fun toString(): String { return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)};" + - " Key: ${signingKey?.toString() ?: "null"};" + " Key: $signingKey;" } /** @@ -38,11 +38,16 @@ data class SignatureVerification( * @param validationException exception that caused the verification to fail */ data class Failure( - val signatureVerification: SignatureVerification, + val signature: PGPSignature, + val signingKey: SubkeyIdentifier?, val validationException: SignatureValidationException ) { + + constructor(verification: SignatureVerification, validationException: SignatureValidationException): + this(verification.signature, verification.signingKey, validationException) + override fun toString(): String { - return "$signatureVerification Failure: ${validationException.message}" + return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)}; Key: ${signingKey?.toString() ?: "null"}; Failure: ${validationException.message}" } } } \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java index 771cb8f1..2c29d62a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -28,9 +28,9 @@ public class MessageMetadataTest { // For the sake of testing though, this is okay. MessageMetadata.Message message = new MessageMetadata.Message(); - MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP, message.depth + 1); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.depth + 1); - MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.depth + 1); + MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP, message.getDepth() + 1); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.getDepth() + 1); + MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.getDepth() + 1); MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData(); message.setChild(compressedData);