From 68af0a4f0e984aff2f5218d29f667457e00f6706 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 12 Sep 2023 21:55:39 +0200 Subject: [PATCH] Introduce more extension methods --- .../src/main/kotlin/openpgp/DateExtensions.kt | 23 ++++++ .../extensions/PGPKeyRingExtensions.kt | 16 +++- .../extensions/PGPPublicKeyExtensions.kt | 13 +++ .../extensions/PGPSecretKeyExtensions.kt | 15 ++++ .../extensions/PGPSecretKeyRingExtensions.kt | 19 ++++- .../extensions/PGPSignatureExtensions.kt | 62 +++++++++++---- .../org/pgpainless/key/info/KeyRingInfo.kt | 6 +- .../org/pgpainless/key/util/KeyRingUtils.kt | 6 +- .../pgpainless/signature/SignatureUtils.kt | 79 +++++++------------ 9 files changed, 168 insertions(+), 71 deletions(-) create mode 100644 pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt diff --git a/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt new file mode 100644 index 00000000..db98fb44 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package openpgp + +import java.util.* + + /** + * Return a new date which represents this date plus the given amount of seconds added. + * + * Since '0' is a special date value in the OpenPGP specification + * (e.g. '0' means no expiration for expiration dates), this method will return 'null' if seconds is 0. + * + * @param date date + * @param seconds number of seconds to be added + * @return date plus seconds or null if seconds is '0' + */ + fun Date.plusSeconds(seconds: Long): Date? { + require(Long.MAX_VALUE - time > seconds) { "Adding $seconds seconds to this date would cause time to overflow." } + return if (seconds == 0L) null + else Date(this.time + 1000 * seconds) + } diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt index 2a3c85f5..7ccddb42 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt @@ -8,6 +8,7 @@ import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPOnePassSignature import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.PGPainless import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.SubkeyIdentifier @@ -51,11 +52,22 @@ fun PGPKeyRing.getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = * identify the [PGPPublicKey] via its key-ID. */ fun PGPKeyRing.getPublicKeyFor(signature: PGPSignature): PGPPublicKey? = - signature.getFingerprint()?.let { this.getPublicKey(it) } ?: + signature.fingerprint?.let { this.getPublicKey(it) } ?: this.getPublicKey(signature.keyID) /** * Return the [PGPPublicKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */ fun PGPKeyRing.getPublicKeyFor(onePassSignature: PGPOnePassSignature): PGPPublicKey? = - this.getPublicKey(onePassSignature.keyID) \ No newline at end of file + this.getPublicKey(onePassSignature.keyID) + +/** + * Return the [OpenPgpFingerprint] of this OpenPGP key. + */ +val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint + get() = OpenPgpFingerprint.of(this) + +/** + * Return this OpenPGP key as an ASCII armored String. + */ +fun PGPKeyRing.toAsciiArmor(): String = PGPainless.asciiArmor(this) \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt index a1ed72fe..ad51c6f4 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt @@ -11,6 +11,7 @@ import org.bouncycastle.bcpg.EdDSAPublicBCPGKey import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil import org.bouncycastle.openpgp.PGPPublicKey import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.generation.type.eddsa.EdDSACurve /** @@ -35,3 +36,15 @@ fun PGPPublicKey.getCurveName(): String { .let { it to ECUtil.getCurveName(it) } .let { if (it.second != null) return it.second else throw IllegalArgumentException("Unknown curve: ${it.first}") } } + +/** + * Return the [PublicKeyAlgorithm] of this key. + */ +val PGPPublicKey.publicKeyAlgorithm: PublicKeyAlgorithm + get() = PublicKeyAlgorithm.requireFromId(algorithm) + +/** + * Return the [OpenPgpFingerprint] of this key. + */ +val PGPPublicKey.openPgpFingerprint: OpenPgpFingerprint + get() = OpenPgpFingerprint.of(this) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt index f5d4f522..3d759d1a 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt @@ -7,10 +7,13 @@ package org.bouncycastle.extensions import org.bouncycastle.bcpg.S2K import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.exception.KeyIntegrityException import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.protection.UnlockSecretKey import org.pgpainless.util.Passphrase @@ -70,3 +73,15 @@ fun PGPSecretKey?.isDecrypted(): Boolean = (this == null) || (s2KUsage == 0) * @return true if secret key has S2K of type GNU_DUMMY_S2K, false otherwise. */ fun PGPSecretKey?.hasDummyS2K(): Boolean = (this != null) && (s2K?.type == S2K.GNU_DUMMY_S2K) + +/** + * Return the [PublicKeyAlgorithm] of this key. + */ +val PGPSecretKey.publicKeyAlgorithm: PublicKeyAlgorithm + get() = publicKey.publicKeyAlgorithm + +/** + * Return the [OpenPgpFingerprint] of this key. + */ +val PGPSecretKey.openPgpFingerprint: OpenPgpFingerprint + get() = OpenPgpFingerprint.of(this) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt index a4a1621e..d0529d51 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt @@ -4,6 +4,7 @@ package org.bouncycastle.extensions +import openpgp.openPgpKeyId import org.bouncycastle.openpgp.* import org.pgpainless.key.OpenPgpFingerprint @@ -40,13 +41,29 @@ fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean = fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = this.getSecretKey(fingerprint.bytes) +/** + * Return the [PGPSecretKey] with the given key-ID. + * + * @throws NoSuchElementException if the OpenPGP key doesn't contain a secret key with the given key-ID + */ +fun PGPSecretKeyRing.requireSecretKey(keyId: Long): PGPSecretKey = + getSecretKey(keyId) ?: throw NoSuchElementException("OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") + +/** + * Return the [PGPSecretKey] with the given fingerprint. + * + * @throws NoSuchElementException of the OpenPGP key doesn't contain a secret key with the given fingerprint + */ +fun PGPSecretKeyRing.requireSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey = + getSecretKey(fingerprint) ?: throw NoSuchElementException("OpenPGP key does not contain key with fingerprint $fingerprint.") + /** * Return the [PGPSecretKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. * If the [PGPSignature] does not carry an issuer-fingerprint subpacket, fall back to the issuer-keyID subpacket to * identify the [PGPSecretKey] via its key-ID. */ fun PGPSecretKeyRing.getSecretKeyFor(signature: PGPSignature): PGPSecretKey? = - signature.getFingerprint()?.let { this.getSecretKey(it) } ?: + signature.fingerprint?.let { this.getSecretKey(it) } ?: this.getSecretKey(signature.keyID) /** diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt index 80200aa6..a27e68e0 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -4,10 +4,12 @@ package org.bouncycastle.extensions +import openpgp.plusSeconds import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.algorithm.RevocationState +import org.pgpainless.algorithm.SignatureType import org.pgpainless.key.OpenPgpFingerprint -import org.pgpainless.signature.SignatureUtils +import org.pgpainless.key.util.RevocationAttributes.Reason import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import java.util.* @@ -16,42 +18,74 @@ import java.util.* * such a subpacket. */ fun PGPSignature.getKeyExpirationDate(keyCreationDate: Date): Date? = - SignatureUtils.getKeyExpirationDate(keyCreationDate, this) + SignatureSubpacketsUtil.getKeyExpirationTime(this) + ?.let { keyCreationDate.plusSeconds(it.time) } /** * Return the value of the signature ExpirationTime subpacket, or null, if the signature * does not carry such a subpacket. */ -fun PGPSignature.getSignatureExpirationDate(): Date? = - SignatureUtils.getSignatureExpirationDate(this) +val PGPSignature.signatureExpirationDate: Date? + get() = SignatureSubpacketsUtil.getSignatureExpirationTime(this) + ?.let { this.creationTime.plusSeconds(it.time) } /** * Return true, if the signature is expired at the given reference time. */ fun PGPSignature.isExpired(referenceTime: Date = Date()) = - SignatureUtils.isSignatureExpired(this, referenceTime) + signatureExpirationDate?.let { referenceTime >= it } ?: false /** * Return the key-ID of the issuer, determined by examining the IssuerKeyId and IssuerFingerprint * subpackets of the signature. */ -fun PGPSignature.getIssuerKeyId() = SignatureUtils.determineIssuerKeyId(this) +val PGPSignature.issuerKeyId: Long + get() = when (version) { + 2, 3 -> keyID + else -> { + SignatureSubpacketsUtil.getIssuerKeyIdAsLong(this) + ?.let { if (it != 0L) it else null } + ?: fingerprint?.keyId + ?: 0L + } + } /** - * Return true, if the signature was likely issued by the key with the given fingerprint. + * Return true, if the signature was likely issued by a key with the given fingerprint. */ -fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint) = SignatureUtils.wasIssuedBy(fingerprint, this) +fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint): Boolean = + this.fingerprint?.let { it.keyId == fingerprint.keyId } ?: (keyID == fingerprint.keyId) + +/** + * Return true, if the signature was likely issued by a key with the given fingerprint. + * @param fingerprint fingerprint bytes + */ +@Deprecated("Discouraged in favor of method taking an OpenPgpFingerprint.") +fun PGPSignature.wasIssuedBy(fingerprint: ByteArray): Boolean = + try { + wasIssuedBy(OpenPgpFingerprint.parseFromBinary(fingerprint)) + } catch (e : IllegalArgumentException) { + // Unknown fingerprint length / format + false + } /** * Return true, if this signature is a hard revocation. */ -fun PGPSignature.isHardRevocation() = SignatureUtils.isHardRevocation(this) +val PGPSignature.isHardRevocation + get() = when (SignatureType.requireFromCode(signatureType)) { + SignatureType.KEY_REVOCATION, SignatureType.SUBKEY_REVOCATION, SignatureType.CERTIFICATION_REVOCATION -> { + SignatureSubpacketsUtil.getRevocationReason(this) + ?.let { Reason.isHardRevocation(it.revocationReason) } + ?: true // no reason -> hard revocation + } + else -> false // Not a revocation + } fun PGPSignature?.toRevocationState() = if (this == null) RevocationState.notRevoked() - else - if (isHardRevocation()) RevocationState.hardRevoked() - else RevocationState.softRevoked(creationTime) + else if (isHardRevocation) RevocationState.hardRevoked() + else RevocationState.softRevoked(creationTime) -fun PGPSignature.getFingerprint(): OpenPgpFingerprint? = - SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this) +val PGPSignature.fingerprint: OpenPgpFingerprint? + get() = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this) \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt index 79cc8683..df6023c4 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -121,7 +121,7 @@ class KeyRingInfo( val validAndExpiredUserIds: List = userIds.filter { val certification = signatures.userIdCertifications[it] ?: return@filter false val revocation = signatures.userIdRevocations[it] ?: return@filter true - return@filter !revocation.isHardRevocation() && certification.creationTime > revocation.creationTime + return@filter !revocation.isHardRevocation && certification.creationTime > revocation.creationTime } /** @@ -272,7 +272,7 @@ class KeyRingInfo( * @return true, if the given user-ID is hard-revoked. */ fun isHardRevoked(userId: CharSequence): Boolean { - return signatures.userIdRevocations[userId]?.isHardRevocation() ?: false + return signatures.userIdRevocations[userId]?.isHardRevocation ?: false } /** @@ -632,7 +632,7 @@ class KeyRingInfo( } } signatures.userIdRevocations[userId]?.let { rev -> - if (rev.isHardRevocation()) { + if (rev.isHardRevocation) { return false // hard revoked -> invalid } sig.creationTime > rev.creationTime// re-certification after soft revocation? diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt index 22e78410..79215408 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt @@ -8,6 +8,7 @@ import openpgp.openPgpKeyId import org.bouncycastle.bcpg.S2K import org.bouncycastle.bcpg.SecretKeyPacket import org.bouncycastle.extensions.certificate +import org.bouncycastle.extensions.requireSecretKey import org.bouncycastle.openpgp.* import org.bouncycastle.util.Strings import org.pgpainless.exception.MissingPassphraseException @@ -34,9 +35,10 @@ class KeyRingUtils { * @return primary secret key */ @JvmStatic + @Deprecated("Deprecated in favor of PGPSecretKeyRing extension function.", + ReplaceWith("secretKeys.requireSecretKey(keyId)")) fun requirePrimarySecretKeyFrom(secretKeys: PGPSecretKeyRing): PGPSecretKey { - return getPrimarySecretKeyFrom(secretKeys) - ?: throw NoSuchElementException("Provided PGPSecretKeyRing has no primary secret key.") + return secretKeys.requireSecretKey(secretKeys.publicKey.keyID) } /** diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt index 4ca30e86..0edb6d71 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt @@ -4,7 +4,9 @@ package org.pgpainless.signature +import openpgp.plusSeconds import org.bouncycastle.bcpg.sig.KeyExpirationTime +import org.bouncycastle.extensions.* import org.bouncycastle.openpgp.* import org.bouncycastle.util.encoders.Hex import org.bouncycastle.util.io.Streams @@ -17,6 +19,7 @@ import org.pgpainless.util.ArmorUtils import java.io.IOException import java.io.InputStream import java.util.* +import kotlin.math.sign const val MAX_ITERATIONS = 10000 @@ -32,10 +35,10 @@ class SignatureUtils { * @return key expiration date as given by the signature */ @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.getKeyExpirationDate(keyCreationDate)")) fun getKeyExpirationDate(keyCreationDate: Date, signature: PGPSignature): Date? { - val expirationPacket: KeyExpirationTime = SignatureSubpacketsUtil.getKeyExpirationTime(signature) ?: return null - val expiresInSeconds = expirationPacket.time - return datePlusSeconds(keyCreationDate, expiresInSeconds) + return signature.getKeyExpirationDate(keyCreationDate) } /** @@ -46,12 +49,9 @@ class SignatureUtils { * @return expiration date of the signature, or null if it does not expire. */ @JvmStatic - fun getSignatureExpirationDate(signature: PGPSignature): Date? { - val expirationTime = SignatureSubpacketsUtil.getSignatureExpirationTime(signature) ?: return null - - val expiresInSeconds = expirationTime.time - return datePlusSeconds(signature.creationTime, expiresInSeconds) - } + @Deprecated("Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.signatureExpirationDate")) + fun getSignatureExpirationDate(signature: PGPSignature): Date? = signature.signatureExpirationDate /** * Return a new date which represents the given date plus the given amount of seconds added. @@ -64,11 +64,10 @@ class SignatureUtils { * @return date plus seconds or null if seconds is '0' */ @JvmStatic + @Deprecated("Deprecated in favor of Date extension method.", + ReplaceWith("date.plusSeconds(seconds)")) fun datePlusSeconds(date: Date, seconds: Long): Date? { - if (seconds == 0L) { - return null - } - return Date(date.time + 1000 * seconds) + return date.plusSeconds(seconds) } /** @@ -79,8 +78,10 @@ class SignatureUtils { * @return true if expired, false otherwise */ @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.isExpired()")) fun isSignatureExpired(signature: PGPSignature): Boolean { - return isSignatureExpired(signature, Date()) + return signature.isExpired() } /** @@ -92,9 +93,10 @@ class SignatureUtils { * @return true if sig is expired at reference date, false otherwise */ @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.isExpired(referenceTime)")) fun isSignatureExpired(signature: PGPSignature, referenceTime: Date): Boolean { - val expirationDate = getSignatureExpirationDate(signature) ?: return false - return referenceTime >= expirationDate + return signature.isExpired(referenceTime) } /** @@ -106,15 +108,10 @@ class SignatureUtils { * @return true if signature is a hard revocation */ @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension function.", + ReplaceWith("signature.isHardRevocation()")) fun isHardRevocation(signature: PGPSignature): Boolean { - val type = SignatureType.requireFromCode(signature.signatureType) - if (type != SignatureType.KEY_REVOCATION && type != SignatureType.SUBKEY_REVOCATION && type != SignatureType.CERTIFICATION_REVOCATION) { - // Not a revocation - return false - } - - val reason = SignatureSubpacketsUtil.getRevocationReason(signature) ?: return true // no reason -> hard revocation - return Reason.isHardRevocation(reason.revocationReason) + return signature.isHardRevocation } @JvmStatic @@ -181,22 +178,10 @@ class SignatureUtils { * @return signatures issuing key id */ @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.issuerKeyId")) fun determineIssuerKeyId(signature: PGPSignature): Long { - if (signature.version == 3) { - // V3 sigs do not contain subpackets - return signature.keyID - } - - val issuerKeyId = SignatureSubpacketsUtil.getIssuerKeyId(signature) - val issuerFingerprint = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature) - - if (issuerKeyId != null && issuerKeyId.keyID != 0L) { - return issuerKeyId.keyID - } - if (issuerKeyId == null && issuerFingerprint != null) { - return issuerFingerprint.keyId - } - return 0 + return signature.issuerKeyId } /** @@ -211,21 +196,17 @@ class SignatureUtils { } @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension method", + ReplaceWith("signature.wasIssuedBy(fingerprint)")) fun wasIssuedBy(fingerprint: ByteArray, signature: PGPSignature): Boolean { - return try { - val pgpFingerprint = OpenPgpFingerprint.parseFromBinary(fingerprint) - wasIssuedBy(pgpFingerprint, signature) - } catch (e : IllegalArgumentException) { - // Unknown fingerprint length - false - } + return signature.wasIssuedBy(fingerprint) } @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension method", + ReplaceWith("signature.wasIssuedBy(fingerprint)")) fun wasIssuedBy(fingerprint: OpenPgpFingerprint, signature: PGPSignature): Boolean { - val issuerFp = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature) - ?: return fingerprint.keyId == signature.keyID - return fingerprint == issuerFp + return signature.wasIssuedBy(fingerprint) } /**