pgpainless/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt

160 lines
5.7 KiB
Kotlin

// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.cleartext_signatures
import java.io.*
import kotlin.jvm.Throws
import org.bouncycastle.bcpg.ArmoredInputStream
import org.bouncycastle.openpgp.PGPSignatureList
import org.bouncycastle.util.Strings
import org.pgpainless.exception.WrongConsumingMethodException
import org.pgpainless.implementation.ImplementationFactory
import org.pgpainless.util.ArmoredInputStreamFactory
/**
* Utility class to deal with cleartext-signed messages. Based on Bouncycastle's
* [org.bouncycastle.openpgp.examples.ClearSignedFileProcessor].
*/
class ClearsignedMessageUtil {
companion object {
/**
* Dearmor a clearsigned message, detach the inband signatures and write the plaintext
* message to the provided messageOutputStream.
*
* @param clearsignedInputStream input stream containing a clearsigned message
* @param messageOutputStream output stream to which the dearmored message shall be written
* @return signatures
* @throws IOException if the message is not clearsigned or some other IO error happens
* @throws WrongConsumingMethodException in case the armored message is not cleartext signed
*/
@JvmStatic
@Throws(WrongConsumingMethodException::class, IOException::class)
fun detachSignaturesFromInbandClearsignedMessage(
clearsignedInputStream: InputStream,
messageOutputStream: OutputStream
): PGPSignatureList {
val input: ArmoredInputStream =
if (clearsignedInputStream is ArmoredInputStream) {
clearsignedInputStream
} else {
ArmoredInputStreamFactory.get(clearsignedInputStream)
}
if (!input.isClearText) {
throw WrongConsumingMethodException(
"Message isn't using the Cleartext Signature Framework.")
}
BufferedOutputStream(messageOutputStream).use { output ->
val lineOut = ByteArrayOutputStream()
var lookAhead = readInputLine(lineOut, input)
val lineSep = getLineSeparator()
if (lookAhead != -1 && input.isClearText) {
var line = lineOut.toByteArray()
output.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line))
while (lookAhead != -1 && input.isClearText) {
lookAhead = readInputLine(lineOut, lookAhead, input)
line = lineOut.toByteArray()
output.write(lineSep)
output.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line))
}
} else {
if (lookAhead != -1) {
val line = lineOut.toByteArray()
output.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line))
}
}
}
val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(input)
val next = objectFactory.nextObject() ?: PGPSignatureList(arrayOf())
return next as PGPSignatureList
}
@JvmStatic
private fun readInputLine(bOut: ByteArrayOutputStream, fIn: InputStream): Int {
bOut.reset()
var lookAhead = -1
var ch: Int
while (fIn.read().also { ch = it } >= 0) {
bOut.write(ch)
if (ch == '\r'.code || ch == '\n'.code) {
lookAhead = readPassedEOL(bOut, ch, fIn)
break
}
}
return lookAhead
}
@JvmStatic
private fun readInputLine(
bOut: ByteArrayOutputStream,
lookAhead: Int,
fIn: InputStream
): Int {
var mLookAhead = lookAhead
bOut.reset()
var ch = mLookAhead
do {
bOut.write(ch)
if (ch == '\r'.code || ch == '\n'.code) {
mLookAhead = readPassedEOL(bOut, ch, fIn)
break
}
} while (fIn.read().also { ch = it } >= 0)
if (ch < 0) {
mLookAhead = -1
}
return mLookAhead
}
@JvmStatic
private fun readPassedEOL(bOut: ByteArrayOutputStream, lastCh: Int, fIn: InputStream): Int {
var lookAhead = fIn.read()
if (lastCh == '\r'.code && lookAhead == '\n'.code) {
bOut.write(lookAhead)
lookAhead = fIn.read()
}
return lookAhead
}
@JvmStatic
private fun getLineSeparator(): ByteArray {
val nl = Strings.lineSeparator()
val nlBytes = ByteArray(nl.length)
for (i in nlBytes.indices) {
nlBytes[i] = nl[i].code.toByte()
}
return nlBytes
}
@JvmStatic
private fun getLengthWithoutSeparatorOrTrailingWhitespace(line: ByteArray): Int {
var end = line.size - 1
while (end >= 0 && isWhiteSpace(line[end])) {
end--
}
return end + 1
}
@JvmStatic
private fun isLineEnding(b: Byte): Boolean {
return b == '\r'.code.toByte() || b == '\n'.code.toByte()
}
@JvmStatic
private fun isWhiteSpace(b: Byte): Boolean {
return isLineEnding(b) || b == '\t'.code.toByte() || b == ' '.code.toByte()
}
}
}