From 53b1e3ff71e78273e38b93eea040159e99ef2be3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 26 Sep 2023 14:47:05 +0200 Subject: [PATCH] Kotlin conversion: HashContextSigning --- .../BcHashContextSigner.java | 66 ------- .../BcPGPHashContextContentSignerBuilder.java | 174 ------------------ .../PGPHashContextContentSignerBuilder.java | 83 --------- .../encryption_signing/package-info.java | 8 - .../encryption_signing/BcHashContextSigner.kt | 49 +++++ .../BcPGPHashContextContentSignerBuilder.kt | 131 +++++++++++++ .../PGPHashContextContentSignerBuilder.kt | 42 +++++ 7 files changed, 222 insertions(+), 331 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java deleted file mode 100644 index 54c6df9e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.security.MessageDigest; -import java.util.List; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; - -import javax.annotation.Nonnull; - -public class BcHashContextSigner { - - public static PGPSignature signHashContext(@Nonnull MessageDigest hashContext, - @Nonnull SignatureType signatureType, - @Nonnull PGPSecretKeyRing secretKeys, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - List signingSubkeyCandidates = info.getSigningSubkeys(); - PGPSecretKey signingKey = null; - for (PGPPublicKey signingKeyCandidate : signingSubkeyCandidates) { - signingKey = secretKeys.getSecretKey(signingKeyCandidate.getKeyID()); - if (signingKey != null) { - break; - } - } - if (signingKey == null) { - throw new PGPException("Key does not contain suitable signing subkey."); - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(signingKey, protector); - return signHashContext(hashContext, signatureType, privateKey); - } - - /** - * Create an OpenPGP Signature over the given {@link MessageDigest} hash context. - * - * @param hashContext hash context - * @param privateKey signing-capable key - * @return signature - * @throws PGPException in case of an OpenPGP error - */ - static PGPSignature signHashContext(MessageDigest hashContext, SignatureType signatureType, PGPPrivateKey privateKey) - throws PGPException { - PGPSignatureGenerator sigGen = new PGPSignatureGenerator( - new BcPGPHashContextContentSignerBuilder(hashContext) - ); - - sigGen.init(signatureType.getCode(), privateKey); - return sigGen.generate(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java deleted file mode 100644 index 5cdf9e36..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.io.OutputStream; -import java.security.MessageDigest; - -import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; -import org.bouncycastle.crypto.CipherParameters; -import org.bouncycastle.crypto.CryptoException; -import org.bouncycastle.crypto.DataLengthException; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.Signer; -import org.bouncycastle.crypto.params.AsymmetricKeyParameter; -import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; -import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; -import org.bouncycastle.crypto.signers.DSADigestSigner; -import org.bouncycastle.crypto.signers.DSASigner; -import org.bouncycastle.crypto.signers.ECDSASigner; -import org.bouncycastle.crypto.signers.Ed25519Signer; -import org.bouncycastle.crypto.signers.Ed448Signer; -import org.bouncycastle.crypto.signers.RSADigestSigner; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.operator.PGPContentSigner; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter; -import org.bouncycastle.util.Arrays; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; - -/** - * Implementation of {@link PGPContentSignerBuilder} using the BC API, which can be used to sign hash contexts. - * This can come in handy to sign data, which was already processed to calculate the hash context, without the - * need to process it again to calculate the OpenPGP signature. - */ -class BcPGPHashContextContentSignerBuilder extends PGPHashContextContentSignerBuilder { - - private final BcPGPKeyConverter keyConverter = new BcPGPKeyConverter(); - private final MessageDigest messageDigest; - private final HashAlgorithm hashAlgorithm; - - BcPGPHashContextContentSignerBuilder(MessageDigest messageDigest) { - this.messageDigest = messageDigest; - this.hashAlgorithm = requireFromName(messageDigest.getAlgorithm()); - } - - private static HashAlgorithm requireFromName(String digestName) { - HashAlgorithm hashAlgorithm = HashAlgorithm.fromName(digestName); - if (hashAlgorithm == null) { - throw new IllegalArgumentException("Cannot recognize OpenPGP Hash Algorithm: " + digestName); - } - return hashAlgorithm; - } - - @Override - public PGPContentSigner build(int signatureType, PGPPrivateKey privateKey) throws PGPException { - PublicKeyAlgorithm keyAlgorithm = PublicKeyAlgorithm.requireFromId(privateKey.getPublicKeyPacket().getAlgorithm()); - AsymmetricKeyParameter privKeyParam = keyConverter.getPrivateKey(privateKey); - final Signer signer = createSigner(keyAlgorithm, messageDigest, privKeyParam); - signer.init(true, privKeyParam); - - return new PGPContentSigner() { - public int getType() { - return signatureType; - } - - public int getHashAlgorithm() { - return hashAlgorithm.getAlgorithmId(); - } - - public int getKeyAlgorithm() { - return keyAlgorithm.getAlgorithmId(); - } - - public long getKeyID() { - return privateKey.getKeyID(); - } - - public OutputStream getOutputStream() { - return new PGPHashContextContentSignerBuilder.SignerOutputStream(signer); - } - - public byte[] getSignature() { - try { - return signer.generateSignature(); - } catch (CryptoException e) { - throw new IllegalStateException("unable to create signature"); - } - } - - public byte[] getDigest() { - return messageDigest.digest(); - } - }; - } - - static Signer createSigner( - PublicKeyAlgorithm keyAlgorithm, - MessageDigest messageDigest, - CipherParameters keyParam) - throws PGPException { - ExistingMessageDigest staticDigest = new ExistingMessageDigest(messageDigest); - switch (keyAlgorithm.getAlgorithmId()) { - case PublicKeyAlgorithmTags.RSA_GENERAL: - case PublicKeyAlgorithmTags.RSA_SIGN: - return new RSADigestSigner(staticDigest); - case PublicKeyAlgorithmTags.DSA: - return new DSADigestSigner(new DSASigner(), staticDigest); - case PublicKeyAlgorithmTags.ECDSA: - return new DSADigestSigner(new ECDSASigner(), staticDigest); - case PublicKeyAlgorithmTags.EDDSA: - if (keyParam instanceof Ed25519PrivateKeyParameters || keyParam instanceof Ed25519PublicKeyParameters) { - return new EdDsaSigner(new Ed25519Signer(), staticDigest); - } - return new EdDsaSigner(new Ed448Signer(new byte[0]), staticDigest); - default: - throw new PGPException("cannot recognise keyAlgorithm: " + keyAlgorithm); - } - } - - // Copied from BCs BcImplProvider - required since BCs class is package visible only :/ - private static class EdDsaSigner - implements Signer { - private final Signer signer; - private final Digest digest; - private final byte[] digBuf; - - EdDsaSigner(Signer signer, Digest digest) { - this.signer = signer; - this.digest = digest; - this.digBuf = new byte[digest.getDigestSize()]; - } - - public void init(boolean forSigning, CipherParameters param) { - this.signer.init(forSigning, param); - this.digest.reset(); - } - - public void update(byte b) { - this.digest.update(b); - } - - public void update(byte[] in, int off, int len) { - this.digest.update(in, off, len); - } - - public byte[] generateSignature() - throws CryptoException, DataLengthException { - digest.doFinal(digBuf, 0); - - signer.update(digBuf, 0, digBuf.length); - - return signer.generateSignature(); - } - - public boolean verifySignature(byte[] signature) { - digest.doFinal(digBuf, 0); - - signer.update(digBuf, 0, digBuf.length); - - return signer.verifySignature(signature); - } - - public void reset() { - Arrays.clear(digBuf); - signer.reset(); - digest.reset(); - } - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java deleted file mode 100644 index 7b8529fe..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.io.OutputStream; -import java.security.MessageDigest; -import javax.annotation.Nonnull; - -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.Signer; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; - -abstract class PGPHashContextContentSignerBuilder implements PGPContentSignerBuilder { - - // Copied from BC, required since BCs class is package visible only - static class SignerOutputStream - extends OutputStream { - private Signer sig; - - SignerOutputStream(Signer sig) { - this.sig = sig; - } - - public void write(@Nonnull byte[] bytes, int off, int len) { - sig.update(bytes, off, len); - } - - public void write(@Nonnull byte[] bytes) { - sig.update(bytes, 0, bytes.length); - } - - public void write(int b) { - sig.update((byte) b); - } - } - - - static class ExistingMessageDigest implements Digest { - - private final MessageDigest digest; - - ExistingMessageDigest(MessageDigest messageDigest) { - this.digest = messageDigest; - } - - @Override - public void update(byte in) { - digest.update(in); - } - - @Override - public void update(byte[] in, int inOff, int len) { - digest.update(in, inOff, len); - } - - @Override - public int doFinal(byte[] out, int outOff) { - byte[] hash = digest.digest(); - System.arraycopy(hash, 0, out, outOff, hash.length); - return getDigestSize(); - } - - @Override - public void reset() { - // Nope! - // We cannot reset, since BCs signer classes are resetting in their init() methods, which would also reset - // the messageDigest, losing its state. This would shatter our intention. - } - - @Override - public String getAlgorithmName() { - return digest.getAlgorithm(); - } - - @Override - public int getDigestSize() { - return digest.getDigestLength(); - } - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/package-info.java deleted file mode 100644 index 4a2f1f39..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes used to encrypt or sign data using OpenPGP. - */ -package org.pgpainless.encryption_signing; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt new file mode 100644 index 00000000..90f275a4 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.extensions.unlock +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureGenerator +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import java.security.MessageDigest + +class BcHashContextSigner { + + companion object { + @JvmStatic + fun signHashContext(hashContext: MessageDigest, + signatureType: SignatureType, + secretKey: PGPSecretKeyRing, + protector: SecretKeyRingProtector): PGPSignature { + val info = PGPainless.inspectKeyRing(secretKey) + return info.signingSubkeys.mapNotNull { info.getSecretKey(it.keyID) }.firstOrNull() + ?.let { signHashContext(hashContext, signatureType, it.unlock(protector)) } + ?: throw PGPException("Key does not contain suitable signing subkey.") + } + + /** + * Create an OpenPGP Signature over the given [MessageDigest] hash context. + * + * @param hashContext hash context + * @param privateKey signing-capable key + * @return signature + * @throws PGPException in case of an OpenPGP error + */ + @JvmStatic + internal fun signHashContext(hashContext: MessageDigest, + signatureType: SignatureType, + privateKey: PGPPrivateKey): PGPSignature { + return PGPSignatureGenerator(BcPGPHashContextContentSignerBuilder(hashContext)) + .apply { init(signatureType.code, privateKey) } + .generate() + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt new file mode 100644 index 00000000..381c4716 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags +import org.bouncycastle.crypto.CipherParameters +import org.bouncycastle.crypto.CryptoException +import org.bouncycastle.crypto.Digest +import org.bouncycastle.crypto.Signer +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters +import org.bouncycastle.crypto.signers.DSADigestSigner +import org.bouncycastle.crypto.signers.DSASigner +import org.bouncycastle.crypto.signers.ECDSASigner +import org.bouncycastle.crypto.signers.Ed25519Signer +import org.bouncycastle.crypto.signers.Ed448Signer +import org.bouncycastle.crypto.signers.RSADigestSigner +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.operator.PGPContentSigner +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm +import java.io.OutputStream +import java.security.MessageDigest + +/** + * Implementation of [PGPContentSignerBuilder] using the BC API, which can be used to sign hash contexts. + * This can come in handy to sign data, which was already processed to calculate the hash context, without the + * need to process it again to calculate the OpenPGP signature. + */ +class BcPGPHashContextContentSignerBuilder( + private val messageDigest: MessageDigest +) : PGPHashContextContentSignerBuilder() { + + private val keyConverter = BcPGPKeyConverter() + private val _hashAlgorithm: HashAlgorithm + + init { + _hashAlgorithm = requireFromName(messageDigest.algorithm) + } + + override fun build(signatureType: Int, privateKey: PGPPrivateKey): PGPContentSigner { + val keyAlgorithm = PublicKeyAlgorithm.requireFromId(privateKey.publicKeyPacket.algorithm) + val privKeyParam = keyConverter.getPrivateKey(privateKey) + val signer = createSigner(keyAlgorithm, messageDigest, privKeyParam) + signer.init(true, privKeyParam) + + return object : PGPContentSigner { + override fun getOutputStream(): OutputStream = SignerOutputStream(signer) + override fun getSignature(): ByteArray = try { + signer.generateSignature() + } catch (e : CryptoException) { + throw IllegalStateException("unable to create signature.", e) + } + override fun getDigest(): ByteArray = messageDigest.digest() + override fun getType(): Int = signatureType + override fun getHashAlgorithm(): Int = _hashAlgorithm.algorithmId + override fun getKeyAlgorithm(): Int = keyAlgorithm.algorithmId + override fun getKeyID(): Long = privateKey.keyID + } + } + + companion object { + @JvmStatic + private fun requireFromName(digestName: String): HashAlgorithm { + val algorithm = HashAlgorithm.fromName(digestName) + require(algorithm != null) { "Cannot recognize OpenPGP Hash Algorithm: $digestName"} + return algorithm + } + + @JvmStatic + private fun createSigner(keyAlgorithm: PublicKeyAlgorithm, + messageDigest: MessageDigest, + keyParam: CipherParameters): Signer { + val staticDigest = ExistingMessageDigest(messageDigest) + return when (keyAlgorithm.algorithmId) { + PublicKeyAlgorithmTags.RSA_GENERAL, PublicKeyAlgorithmTags.RSA_SIGN -> RSADigestSigner(staticDigest) + PublicKeyAlgorithmTags.DSA -> DSADigestSigner(DSASigner(), staticDigest) + PublicKeyAlgorithmTags.ECDSA -> DSADigestSigner(ECDSASigner(), staticDigest) + PublicKeyAlgorithmTags.EDDSA_LEGACY -> { + if (keyParam is Ed25519PrivateKeyParameters || keyParam is Ed25519PublicKeyParameters) + EdDsaSigner(Ed25519Signer(), staticDigest) + else EdDsaSigner(Ed448Signer(byteArrayOf()), staticDigest) + } + else -> throw PGPException("cannot recognize keyAlgorithm: $keyAlgorithm") + } + } + } + + // Copied from BCs BcImplProvider - required since BCs class is package visible only :/ + internal class EdDsaSigner( + private val signer: Signer, + private val digest: Digest + ) : Signer { + private val digBuf: ByteArray = ByteArray(digest.digestSize) + + override fun init(forSigning: Boolean, param: CipherParameters) { + signer.init(forSigning, param) + digest.reset() + } + + override fun update(b: Byte) { + digest.update(b) + } + + override fun update(b: ByteArray, off: Int, len: Int) { + digest.update(b, off, len) + } + + override fun generateSignature(): ByteArray { + digest.doFinal(digBuf, 0) + signer.update(digBuf, 0, digBuf.size) + return signer.generateSignature() + } + + override fun verifySignature(signature: ByteArray): Boolean { + digest.doFinal(digBuf, 0) + signer.update(digBuf, 0, digBuf.size) + return signer.verifySignature(signature) + } + + override fun reset() { + digBuf.fill(0) + signer.reset() + digest.reset() + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt new file mode 100644 index 00000000..aa3dd58c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.crypto.Digest +import org.bouncycastle.crypto.Signer +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder +import java.io.OutputStream +import java.security.MessageDigest + +abstract class PGPHashContextContentSignerBuilder : PGPContentSignerBuilder { + + // Copied from BC, required since BCs class is package visible only + internal class SignerOutputStream( + private val signer: Signer + ) : OutputStream() { + override fun write(p0: Int) = signer.update(p0.toByte()) + override fun write(b: ByteArray) = signer.update(b, 0, b.size) + override fun write(b: ByteArray, off: Int, len: Int) = signer.update(b, off, len) + } + + internal class ExistingMessageDigest( + private val digest: MessageDigest + ) : Digest { + + override fun getAlgorithmName(): String = digest.algorithm + override fun getDigestSize(): Int = digest.digestLength + override fun update(b: Byte) = digest.update(b) + override fun update(buf: ByteArray, inOff: Int, len: Int) = digest.update(buf) + override fun doFinal(out: ByteArray, outOff: Int): Int { + digest.digest().copyInto(out, outOff) + return digestSize + } + override fun reset() { + // Nope! + // We cannot reset, since BCs signer classes are resetting in their init() methods, which would also reset + // the messageDigest, losing its state. This would shatter our intention. + } + } +} \ No newline at end of file