// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 package org.pgpainless.encryption_signing import java.io.BufferedOutputStream import java.io.IOException import java.io.OutputStream import org.bouncycastle.bcpg.ArmoredOutputStream import org.bouncycastle.bcpg.BCPGOutputStream import org.bouncycastle.openpgp.PGPCompressedDataGenerator import org.bouncycastle.openpgp.PGPEncryptedDataGenerator import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPLiteralDataGenerator import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.util.ArmoredOutputStreamFactory import org.slf4j.LoggerFactory // 1 << 8 causes wrong partial body length encoding // 1 << 9 fixes this. // see https://github.com/pgpainless/pgpainless/issues/160 const val BUFFER_SIZE = 1 shl 9 /** * OutputStream that produces an OpenPGP message. The message can be encrypted, signed, or both, * depending on its configuration. * * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. * * @see Source */ class EncryptionStream( private var outermostStream: OutputStream, private val options: ProducerOptions, ) : OutputStream() { private val resultBuilder: EncryptionResult.Builder = EncryptionResult.builder() private var closed: Boolean = false private var signatureLayerStream: OutputStream? = null private var armorOutputStream: ArmoredOutputStream? = null private var publicKeyEncryptedStream: OutputStream? = null private var compressedDataGenerator: PGPCompressedDataGenerator? = null private var basicCompressionStream: BCPGOutputStream? = null private var literalDataGenerator: PGPLiteralDataGenerator? = null private var literalDataStream: OutputStream? = null init { prepareArmor() prepareEncryption() prepareCompression() prepareOnePassSignatures() prepareLiteralDataProcessing() prepareSigningStream() prepareInputEncoding() } private fun prepareArmor() { if (!options.isAsciiArmor) { LOGGER.debug("Output will be unarmored.") return } outermostStream = BufferedOutputStream(outermostStream) LOGGER.debug("Wrap encryption output in ASCII armor.") armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream, options).also { outermostStream = it } } @Throws(IOException::class, PGPException::class) private fun prepareEncryption() { if (options.encryptionOptions == null) { // No encryption options -> no encryption resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL) return } require(options.encryptionOptions.encryptionMethods.isNotEmpty()) { "If EncryptionOptions are provided, at least one encryption method MUST be provided as well." } EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(options.encryptionOptions).let { resultBuilder.setEncryptionAlgorithm(it) LOGGER.debug("Encrypt message using symmetric algorithm $it.") val encryptedDataGenerator = PGPEncryptedDataGenerator( ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(it).apply { setWithIntegrityPacket(true) }) options.encryptionOptions.encryptionMethods.forEach { m -> encryptedDataGenerator.addMethod(m) } options.encryptionOptions.encryptionKeyIdentifiers.forEach { r -> resultBuilder.addRecipient(r) } publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)).also { stream -> outermostStream = stream } } } @Throws(IOException::class) private fun prepareCompression() { EncryptionBuilder.negotiateCompressionAlgorithm(options).let { resultBuilder.setCompressionAlgorithm(it) compressedDataGenerator = PGPCompressedDataGenerator(it.algorithmId) if (it == CompressionAlgorithm.UNCOMPRESSED) return LOGGER.debug("Compress using $it.") basicCompressionStream = BCPGOutputStream(compressedDataGenerator!!.open(outermostStream)).also { stream -> outermostStream = stream } } } @Throws(IOException::class, PGPException::class) private fun prepareOnePassSignatures() { signatureLayerStream = outermostStream if (options.signingOptions == null) { return } require(options.signingOptions.signingMethods.isNotEmpty()) { "If SigningOptions are provided, at least one SigningMethod MUST be provided." } for ((index, method) in options.signingOptions.signingMethods.values.withIndex()) { if (!method.isDetached) { // The last sig is not nested, all others are val nested = index + 1 < options.signingOptions.signingMethods.size method.signatureGenerator.generateOnePassVersion(nested).encode(outermostStream) } } } @Throws(IOException::class) private fun prepareLiteralDataProcessing() { if (options.isCleartextSigned) { val hashAlgorithms = collectHashAlgorithmsForCleartextSigning() armorOutputStream!!.beginClearText(*hashAlgorithms.toIntArray()) return } literalDataGenerator = PGPLiteralDataGenerator().also { gen -> literalDataStream = gen.open( outermostStream, options.encoding.code, options.fileName, options.modificationDate, ByteArray(BUFFER_SIZE)) .also { stream -> outermostStream = stream } } resultBuilder.apply { setFileName(options.fileName) setModificationDate(options.modificationDate) setFileEncoding(options.encoding) } } private fun prepareSigningStream() { outermostStream = SignatureGenerationStream(outermostStream, options.signingOptions) } private fun prepareInputEncoding() { outermostStream = CRLFGeneratorStream( // By buffering here, we drastically improve performance // Reason is that CRLFGeneratorStream only implements write(int), so we need // BufferedOutputStream to // "convert" to write(buf) calls again BufferedOutputStream(outermostStream), if (options.isApplyCRLFEncoding) StreamEncoding.UTF8 else StreamEncoding.BINARY) } private fun collectHashAlgorithmsForCleartextSigning(): Array { return options.signingOptions ?.signingMethods ?.values ?.map { it.hashAlgorithm } ?.toSet() ?.map { it.algorithmId } ?.toTypedArray() ?: arrayOf() } @Throws(IOException::class) override fun write(data: Int) = outermostStream.write(data) @Throws(IOException::class) override fun write(buffer: ByteArray) = write(buffer, 0, buffer.size) @Throws(IOException::class) override fun write(buffer: ByteArray, off: Int, len: Int) = outermostStream.write(buffer, off, len) @Throws(IOException::class) override fun flush() = outermostStream.flush() @Throws(IOException::class) override fun close() { if (closed) return outermostStream.close() literalDataStream?.apply { flush() close() } literalDataGenerator?.close() if (options.isCleartextSigned) { armorOutputStream?.apply { write('\r'.code) write('\n'.code) endClearText() } } try { writeSignatures() } catch (e: PGPException) { throw IOException("Exception while writing signatures.", e) } compressedDataGenerator?.close() publicKeyEncryptedStream?.apply { flush() close() } armorOutputStream?.apply { flush() close() } closed = true } @Throws(PGPException::class, IOException::class) private fun writeSignatures() { if (options.signingOptions == null) { return } options.signingOptions.signingMethods.entries.reversed().forEach { (key, method) -> method.signatureGenerator.generate().let { sig -> if (method.isDetached) { resultBuilder.addDetachedSignature(key, sig) } if (!method.isDetached || options.isCleartextSigned) { sig.encode(signatureLayerStream) } } } } val result: EncryptionResult get() = check(closed) { "EncryptionStream must be closed before accessing the result." } .let { resultBuilder.build() } val isClosed get() = closed companion object { @JvmStatic private val LOGGER = LoggerFactory.getLogger(EncryptionStream::class.java) } }