160 lines
5.7 KiB
Kotlin
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()
|
|
}
|
|
}
|
|
}
|