// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 package org.pgpainless.decryption_verification import java.util.* import javax.annotation.Nonnull 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.bouncycastle.extensions.matches 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 [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.matches(fingerprint) } fun isVerifiedSignedBy(keys: PGPKeyRing) = verifiedSignatures.any { keys.matches(it.signingKey) } fun isVerifiedDetachedSignedBy(fingerprint: OpenPgpFingerprint) = verifiedDetachedSignatures.any { it.signingKey.matches(fingerprint) } fun isVerifiedDetachedSignedBy(keys: PGPKeyRing) = verifiedDetachedSignatures.any { keys.matches(it.signingKey) } fun isVerifiedInlineSignedBy(fingerprint: OpenPgpFingerprint) = verifiedInlineSignatures.any { it.signingKey.matches(fingerprint) } fun isVerifiedInlineSignedBy(keys: PGPKeyRing) = verifiedInlineSignatures.any { keys.matches(it.signingKey) } // ################################################################################################################ // ### 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 } }