// SPDX-FileCopyrightText: 2024 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 package org.pgpainless.sop import java.io.InputStream import java.io.OutputStream import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPSecretKeyRing import org.bouncycastle.openpgp.PGPSignature import org.bouncycastle.util.io.Streams import org.pgpainless.PGPainless import org.pgpainless.algorithm.DocumentSignatureType import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.bouncycastle.extensions.openPgpFingerprint import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.exception.KeyException.MissingSecretKeyException import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException import org.pgpainless.util.ArmoredOutputStreamFactory import org.pgpainless.util.Passphrase import sop.MicAlg import sop.ReadyWithResult import sop.SigningResult import sop.enums.SignAs import sop.exception.SOPGPException import sop.operation.DetachedSign import sop.util.UTF8Util /** Implementation of the `sign` operation using PGPainless. */ class DetachedSignImpl : DetachedSign { private val signingOptions = SigningOptions.get() private val protector = MatchMakingSecretKeyRingProtector() private val signingKeys = mutableListOf() private var armor = true private var mode = SignAs.binary override fun data(data: InputStream): ReadyWithResult { signingKeys.forEach { try { signingOptions.addDetachedSignature(protector, it, modeToSigType(mode)) } catch (e: UnacceptableSigningKeyException) { throw SOPGPException.KeyCannotSign("Key ${it.openPgpFingerprint} cannot sign.", e) } catch (e: MissingSecretKeyException) { throw SOPGPException.KeyCannotSign( "Key ${it.openPgpFingerprint} cannot sign. Missing secret key.", e) } catch (e: PGPException) { throw SOPGPException.KeyIsProtected( "Key ${it.openPgpFingerprint} cannot be unlocked.", e) } } // When creating a detached signature, the output of the signing stream is actually // the unmodified plaintext data, so we can discard it. // The detached signature will later be retrieved from the metadata object instead. val sink = NullOutputStream() try { val signingStream = PGPainless.encryptAndOrSign() .onOutputStream(sink) .withOptions(ProducerOptions.sign(signingOptions).setAsciiArmor(armor)) return object : ReadyWithResult() { override fun writeTo(outputStream: OutputStream): SigningResult { check(!signingStream.isClosed) { "The operation is a one-shot object." } Streams.pipeAll(data, signingStream) signingStream.close() val result = signingStream.result // forget passphrases protector.clear() val signatures = result.detachedSignatures.map { it.value }.flatten() val out = if (armor) ArmoredOutputStreamFactory.get(outputStream) else outputStream signatures.forEach { it.encode(out) } out.close() outputStream.close() return SigningResult.builder() .setMicAlg(micAlgFromSignatures(signatures)) .build() } } } catch (e: PGPException) { throw RuntimeException(e) } } override fun key(key: InputStream): DetachedSign = apply { KeyReader.readSecretKeys(key, true).forEach { val info = PGPainless.inspectKeyRing(it) if (!info.isUsableForSigning) { throw SOPGPException.KeyCannotSign( "Key ${info.fingerprint} does not have valid, signing capable subkeys.") } protector.addSecretKey(it) signingKeys.add(it) } } override fun mode(mode: SignAs): DetachedSign = apply { this.mode = mode } override fun noArmor(): DetachedSign = apply { armor = false } override fun withKeyPassword(password: ByteArray): DetachedSign = apply { protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) } private fun modeToSigType(mode: SignAs): DocumentSignatureType { return when (mode) { SignAs.binary -> DocumentSignatureType.BINARY_DOCUMENT SignAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT } } private fun micAlgFromSignatures(signatures: List): MicAlg = signatures .mapNotNull { HashAlgorithm.fromId(it.hashAlgorithm) } .toSet() .singleOrNull() ?.let { MicAlg.fromHashAlgorithmId(it.algorithmId) } ?: MicAlg.empty() }