1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-12-12 23:31:08 +01:00

Finish Policy conversion and move kotlin classes to src/kotlin/

This commit is contained in:
Paul Schaub 2023-08-23 13:15:00 +02:00
parent b68061373d
commit e57e74163c
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
36 changed files with 371 additions and 896 deletions

View file

@ -0,0 +1,175 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless
import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.PGPSecretKeyRing
import org.bouncycastle.openpgp.PGPSignature
import org.pgpainless.decryption_verification.DecryptionBuilder
import org.pgpainless.encryption_signing.EncryptionBuilder
import org.pgpainless.key.certification.CertifyCertificate
import org.pgpainless.key.generation.KeyRingBuilder
import org.pgpainless.key.generation.KeyRingTemplates
import org.pgpainless.key.info.KeyRingInfo
import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor
import org.pgpainless.key.parsing.KeyRingReader
import org.pgpainless.key.util.KeyRingUtils
import org.pgpainless.policy.Policy
import org.pgpainless.util.ArmorUtils
import java.io.OutputStream
import java.util.*
class PGPainless private constructor() {
companion object {
/**
* Generate a fresh OpenPGP key ring from predefined templates.
* @return templates
*/
@JvmStatic
fun generateKeyRing() = KeyRingTemplates()
/**
* Build a custom OpenPGP key ring.
*
* @return builder
*/
@JvmStatic
fun buildKeyRing() = KeyRingBuilder()
/**
* Read an existing OpenPGP key ring.
* @return builder
*/
@JvmStatic
fun readKeyRing() = KeyRingReader()
/**
* Extract a public key certificate from a secret key.
*
* @param secretKey secret key
* @return public key certificate
*/
@JvmStatic
fun extractCertificate(secretKey: PGPSecretKeyRing) =
KeyRingUtils.publicKeyRingFrom(secretKey)
/**
* Merge two copies of the same certificate (e.g. an old copy, and one retrieved from a key server) together.
*
* @param originalCopy local, older copy of the cert
* @param updatedCopy updated, newer copy of the cert
* @return merged certificate
* @throws PGPException in case of an error
*/
@JvmStatic
fun mergeCertificate(originalCopy: PGPPublicKeyRing,
updatedCopy: PGPPublicKeyRing) =
PGPPublicKeyRing.join(originalCopy, updatedCopy)
/**
* Wrap a key or certificate in ASCII armor.
*
* @param key key or certificate
* @return ascii armored string
*
* @throws IOException in case of an error during the armoring process
*/
@JvmStatic
fun asciiArmor(key: PGPKeyRing) =
if (key is PGPSecretKeyRing)
ArmorUtils.toAsciiArmoredString(key)
else
ArmorUtils.toAsciiArmoredString(key as PGPPublicKeyRing)
/**
* Wrap a key of certificate in ASCII armor and write the result into the given [OutputStream].
*
* @param key key or certificate
* @param outputStream output stream
*
* @throws IOException in case of an error during the armoring process
*/
@JvmStatic
fun asciiArmor(key: PGPKeyRing, outputStream: OutputStream) {
val armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream)
key.encode(armorOut)
armorOut.close()
}
/**
* Wrap the detached signature in ASCII armor.
*
* @param signature detached signature
* @return ascii armored string
*
* @throws IOException in case of an error during the armoring process
*/
@JvmStatic
fun asciiArmor(signature: PGPSignature) = ArmorUtils.toAsciiArmoredString(signature)
/**
* Create an [EncryptionBuilder], which can be used to encrypt and/or sign data using OpenPGP.
*
* @return builder
*/
@JvmStatic
fun encryptAndOrSign() = EncryptionBuilder()
/**
* Create a [DecryptionBuilder], which can be used to decrypt and/or verify data using OpenPGP.
*
* @return builder
*/
@JvmStatic
fun decryptAndOrVerify() = DecryptionBuilder()
/**
* Make changes to a secret key at the given reference time.
* This method can be used to change key expiration dates and passphrases, or add/revoke user-ids and subkeys.
* <p>
* After making the desired changes in the builder, the modified key can be extracted using {@link SecretKeyRingEditorInterface#done()}.
*
* @param secretKeys secret key ring
* @param referenceTime reference time used as signature creation date
* @return builder
*/
@JvmStatic
@JvmOverloads
fun modifyKeyRing(secretKey: PGPSecretKeyRing, referenceTime: Date = Date()) =
SecretKeyRingEditor(secretKey, referenceTime)
/**
* Quickly access information about a [org.bouncycastle.openpgp.PGPPublicKeyRing] / [PGPSecretKeyRing].
* This method can be used to determine expiration dates, key flags and other information about a key at a specific time.
*
* @param keyRing key ring
* @param referenceTime date of inspection
* @return access object
*/
@JvmStatic
@JvmOverloads
fun inspectKeyRing(key: PGPKeyRing, referenceTime: Date = Date()) =
KeyRingInfo(key, referenceTime)
/**
* Access, and make changes to PGPainless policy on acceptable/default algorithms etc.
*
* @return policy
*/
@JvmStatic
fun getPolicy() = Policy.getInstance()
/**
* Create different kinds of signatures on other keys.
*
* @return builder
*/
@JvmStatic
fun certify() = CertifyCertificate()
}
}

View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <info@pgpainless.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
enum class AEADAlgorithm(
val algorithmId: Int,
val ivLength: Int,
val tagLength: Int) {
EAX(1, 16, 16),
OCB(2, 15, 16),
GCM(3, 12, 16),
;
companion object {
@JvmStatic
fun fromId(id: Int): AEADAlgorithm? {
return values().firstOrNull {
algorithm -> algorithm.algorithmId == id
}
}
@JvmStatic
fun requireFromId(id: Int): AEADAlgorithm {
return fromId(id) ?:
throw NoSuchElementException("No AEADAlgorithm found for id $id")
}
}
}

View file

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
class AlgorithmSuite(
symmetricKeyAlgorithms: List<SymmetricKeyAlgorithm>,
hashAlgorithms: List<HashAlgorithm>,
compressionAlgorithms: List<CompressionAlgorithm>) {
val symmetricKeyAlgorithms: Set<SymmetricKeyAlgorithm> = symmetricKeyAlgorithms.toSet()
val hashAlgorithms: Set<HashAlgorithm> = hashAlgorithms.toSet()
val compressionAlgorithms: Set<CompressionAlgorithm> = compressionAlgorithms.toSet()
companion object {
@JvmStatic
val defaultSymmetricKeyAlgorithms = listOf(
SymmetricKeyAlgorithm.AES_256,
SymmetricKeyAlgorithm.AES_192,
SymmetricKeyAlgorithm.AES_128)
@JvmStatic
val defaultHashAlgorithms = listOf(
HashAlgorithm.SHA512,
HashAlgorithm.SHA384,
HashAlgorithm.SHA256,
HashAlgorithm.SHA224)
@JvmStatic
val defaultCompressionAlgorithms = listOf(
CompressionAlgorithm.ZLIB,
CompressionAlgorithm.BZIP2,
CompressionAlgorithm.ZIP,
CompressionAlgorithm.UNCOMPRESSED)
@JvmStatic
val defaultAlgorithmSuite = AlgorithmSuite(
defaultSymmetricKeyAlgorithms,
defaultHashAlgorithms,
defaultCompressionAlgorithms)
}
}

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
enum class CertificationType(
val signatureType: SignatureType
) {
/**
* The issuer of this certification does not make any particular assertion as to how well the certifier has
* checked that the owner of the key is in fact the person described by the User ID.
*/
GENERIC(SignatureType.GENERIC_CERTIFICATION),
/**
* The issuer of this certification has not done any verification of the claim that the owner of this key is
* the User ID specified.
*/
NONE(SignatureType.NO_CERTIFICATION),
/**
* The issuer of this certification has done some casual verification of the claim of identity.
*/
CASUAL(SignatureType.CASUAL_CERTIFICATION),
/**
* The issuer of this certification has done some casual verification of the claim of identity.
*/
POSITIVE(SignatureType.POSITIVE_CERTIFICATION),
;
fun asSignatureType() = signatureType
}

View file

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
/**
* Enumeration of possible compression algorithms.
*
* See also [RFC4880 - Compression Algorithm Tags](https://tools.ietf.org/html/rfc4880#section-9.3)
*/
enum class CompressionAlgorithm(val algorithmId: Int) {
UNCOMPRESSED(0),
ZIP(1),
ZLIB(2),
BZIP2(3),
;
companion object {
/**
* Return the [CompressionAlgorithm] value that corresponds to the provided numerical id.
* If an invalid id is provided, null is returned.
*
* @param id id
* @return compression algorithm
*/
@JvmStatic
fun fromId(id: Int): CompressionAlgorithm? {
return values().firstOrNull {
c -> c.algorithmId == id
}
}
/**
* Return the [CompressionAlgorithm] value that corresponds to the provided numerical id.
* If an invalid id is provided, throw an [NoSuchElementException].
*
* @param id id
* @return compression algorithm
* @throws NoSuchElementException in case of an unmapped id
*/
@JvmStatic
fun requireFromId(id: Int): CompressionAlgorithm {
return fromId(id) ?:
throw NoSuchElementException("No CompressionAlgorithm found for id $id")
}
}
}

View file

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
enum class DocumentSignatureType(val signatureType: SignatureType) {
/**
* Signature is calculated over the unchanged binary data.
*/
BINARY_DOCUMENT(SignatureType.BINARY_DOCUMENT),
/**
* The signature is calculated over the text data with its line endings
* converted to `<CR><LF>`.
*/
CANONICAL_TEXT_DOCUMENT(SignatureType.CANONICAL_TEXT_DOCUMENT),
;
}

View file

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
enum class EncryptionPurpose {
/**
* The stream will encrypt communication that goes over the wire.
* E.g. EMail, Chat...
*/
COMMUNICATIONS,
/**
* The stream will encrypt data at rest.
* E.g. Encrypted backup...
*/
STORAGE,
/**
* The stream will use keys with either flags to encrypt the data.
*/
ANY
}

View file

@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
/**
* An enumeration of features that may be set in the feature subpacket.
*
* See [RFC4880: Features](https://tools.ietf.org/html/rfc4880#section-5.2.3.24)
*/
enum class Feature(val featureId: Byte) {
/**
* Support for Symmetrically Encrypted Integrity Protected Data Packets (version 1) using Modification
* Detection Code Packets.
*
* See [RFC-4880 §5.14: Modification Detection Code Packet](https://tools.ietf.org/html/rfc4880#section-5.14)
*/
MODIFICATION_DETECTION(0x01),
/**
* Support for Authenticated Encryption with Additional Data (AEAD).
* If a key announces this feature, it signals support for consuming AEAD Encrypted Data Packets.
*
* NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!!
* NOTE: This value is currently RESERVED.
*
* See [AEAD Encrypted Data Packet](https://openpgp-wg.gitlab.io/rfc4880bis/#name-aead-encrypted-data-packet-)
*/
GNUPG_AEAD_ENCRYPTED_DATA(0x02),
/**
* If a key announces this feature, it is a version 5 public key.
* The version 5 format is similar to the version 4 format except for the addition of a count for the key material.
* This count helps to parse secret key packets (which are an extension of the public key packet format) in the case
* of an unknown algorithm.
* In addition, fingerprints of version 5 keys are calculated differently from version 4 keys.
*
* NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!!
* NOTE: This value is currently RESERVED.
*
* See [Public-Key Packet Formats](https://openpgp-wg.gitlab.io/rfc4880bis/#name-public-key-packet-formats)
*/
GNUPG_VERSION_5_PUBLIC_KEY(0x04),
/**
* Support for Symmetrically Encrypted Integrity Protected Data packet version 2.
*
* See [crypto-refresh-06 §5.13.2. Version 2 Sym. Encrypted Integrity Protected Data Packet Format](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#version-two-seipd)
*/
MODIFICATION_DETECTION_2(0x08),
;
companion object {
@JvmStatic
fun fromId(id: Byte): Feature? {
return values().firstOrNull {
f -> f.featureId == id
}
}
@JvmStatic
fun requireFromId(id: Byte): Feature {
return fromId(id) ?:
throw NoSuchElementException("Unknown feature id encountered: $id")
}
@JvmStatic
fun fromBitmask(bitmask: Int): List<Feature> {
return values().filter {
it.featureId.toInt() and bitmask != 0
}
}
@JvmStatic
fun toBitmask(vararg features: Feature): Byte {
return features.map { it.featureId.toInt() }
.reduceOrNull { mask, f -> mask or f }?.toByte()
?: 0
}
}
}

View file

@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
/**
* An enumeration of different hashing algorithms.
*
* See [RFC4880: Hash Algorithms](https://tools.ietf.org/html/rfc4880#section-9.4)
*/
enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) {
@Deprecated("MD5 is deprecated")
MD5 (1, "MD5"),
SHA1 (2, "SHA1"),
RIPEMD160 (3, "RIPEMD160"),
SHA256 (8, "SHA256"),
SHA384 (9, "SHA384"),
SHA512 (10, "SHA512"),
SHA224 (11, "SHA224"),
SHA3_256 (12, "SHA3-256"),
SHA3_512 (14, "SHA3-512"),
;
companion object {
/**
* Return the [HashAlgorithm] value that corresponds to the provided algorithm id.
* If an invalid algorithm id was provided, null is returned.
*
* @param id numeric id
* @return enum value
*/
@JvmStatic
fun fromId(id: Int): HashAlgorithm? {
return values().firstOrNull {
h -> h.algorithmId == id
}
}
/**
* Return the [HashAlgorithm] value that corresponds to the provided algorithm id.
* If an invalid algorithm id was provided, throw a [NoSuchElementException].
*
* @param id algorithm id
* @return enum value
* @throws NoSuchElementException in case of an unknown algorithm id
*/
@JvmStatic
fun requireFromId(id: Int): HashAlgorithm {
return fromId(id) ?:
throw NoSuchElementException("No HashAlgorithm found for id $id")
}
/**
* Return the [HashAlgorithm] value that corresponds to the provided name.
* If an invalid algorithm name was provided, null is returned.
*
* See [RFC4880: §9.4 Hash Algorithms](https://datatracker.ietf.org/doc/html/rfc4880#section-9.4)
* for a list of algorithms and names.
*
* @param name text name
* @return enum value
*/
@JvmStatic
fun fromName(name: String): HashAlgorithm? {
return name.uppercase().let { algoName ->
values().firstOrNull {
it.algorithmName == algoName
} ?: values().firstOrNull {
it.algorithmName == algoName.replace("-", "")
}
}
}
}
}

View file

@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
enum class KeyFlag(val flag: Int) {
/**
* This key may be used to certify third-party keys.
*/
CERTIFY_OTHER (1),
/**
* This key may be used to sign data.
*/
SIGN_DATA (2),
/**
* This key may be used to encrypt communications.
*/
ENCRYPT_COMMS (4),
/**
* This key may be used to encrypt storage.
*/
ENCRYPT_STORAGE(8),
/**
* The private component of this key may have been split by a secret-sharing mechanism.
*/
SPLIT (16),
/**
* This key may be used for authentication.
*/
AUTHENTICATION (32),
/**
* The private component of this key may be in the possession of more than one person.
*/
SHARED (128),
;
companion object {
/**
* Convert a bitmask into a list of [KeyFlags][KeyFlag].
*
* @param bitmask bitmask
* @return list of key flags encoded by the bitmask
*/
@JvmStatic
fun fromBitmask(bitmask: Int): List<KeyFlag> {
return values().filter {
it.flag and bitmask != 0
}
}
/**
* Encode a list of {@link KeyFlag KeyFlags} into a bitmask.
*
* @param flags list of flags
* @return bitmask
*/
@JvmStatic
fun toBitmask(vararg flags: KeyFlag): Int {
return flags.map { it.flag }.reduceOrNull { mask, f -> mask or f }
?: 0
}
/**
* Return true if the provided bitmask has the bit for the provided flag set.
* Return false if the mask does not contain the flag.
*
* @param mask bitmask
* @param flag flag to be tested for
* @return true if flag is set, false otherwise
*/
@JvmStatic
fun hasKeyFlag(mask: Int, flag: KeyFlag): Boolean {
return mask and flag.flag == flag.flag
}
@JvmStatic
fun containsAny(mask: Int, vararg flags: KeyFlag): Boolean {
return flags.any {
hasKeyFlag(mask, it)
}
}
}
}

View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
enum class OpenPgpPacket(val tag: Int) {
PKESK(1),
SIG(2),
SKESK(3),
OPS(4),
SK(5),
PK(6),
SSK(7),
COMP(8),
SED(9),
MARKER(10),
LIT(11),
TRUST(12),
UID(13),
PSK(14),
UATTR(17),
SEIPD(18),
MDC(19),
EXP_1(60),
EXP_2(61),
EXP_3(62),
EXP_4(63),
;
companion object {
@JvmStatic
fun fromTag(tag: Int): OpenPgpPacket? {
return values().firstOrNull {
it.tag == tag
}
}
@JvmStatic
fun requireFromTag(tag: Int): OpenPgpPacket {
return fromTag(tag) ?:
throw NoSuchElementException("No OpenPGP packet known for tag $tag")
}
}
}

View file

@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
/**
* Enumeration of public key algorithms as defined in RFC4880.
*
* See [RFC4880: Public-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.1)
*/
enum class PublicKeyAlgorithm(
val algorithmId: Int,
val signingCapable: Boolean,
val encryptionCapable: Boolean) {
/**
* RSA capable of encryption and signatures.
*/
RSA_GENERAL (1, true, true),
/**
* RSA with usage encryption.
*
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation notice</a>
*/
@Deprecated("RSA_ENCRYPT is deprecated in favor of RSA_GENERAL",
ReplaceWith("RSA_GENERAL"))
RSA_ENCRYPT (2, false, true),
/**
* RSA with usage of creating signatures.
*
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation notice</a>
*/
@Deprecated("RSA_SIGN is deprecated in favor of RSA_GENERAL",
ReplaceWith("RSA_GENERAL"))
RSA_SIGN (3, true, false),
/**
* ElGamal with usage encryption.
*/
ELGAMAL_ENCRYPT (16, false, true),
/**
* Digital Signature Algorithm.
*/
DSA (17, true, false),
/**
* Elliptic Curve Diffie-Hellman.
*/
ECDH (18, false, true),
/**
* Elliptic Curve Digital Signature Algorithm.
*/
ECDSA (19, true, false),
/**
* ElGamal General.
*
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.8">Deprecation notice</a>
*/
@Deprecated("ElGamal is deprecated")
ELGAMAL_GENERAL (20, true, true),
/**
* Diffie-Hellman key exchange algorithm.
*/
DIFFIE_HELLMAN (21, false, true),
/**
* Digital Signature Algorithm based on twisted Edwards Curves.
*/
EDDSA (22, true, false),
;
fun isSigningCapable(): Boolean = signingCapable
fun isEncryptionCapable(): Boolean = encryptionCapable
companion object {
@JvmStatic
fun fromId(id: Int): PublicKeyAlgorithm? {
return values().firstOrNull {
it.algorithmId == id
}
}
@JvmStatic
fun requireFromId(id: Int): PublicKeyAlgorithm {
return fromId(id) ?:
throw NoSuchElementException("No PublicKeyAlgorithm found for id $id")
}
}
}

View file

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
import org.pgpainless.util.DateUtil
import java.lang.AssertionError
import java.util.*
import kotlin.NoSuchElementException
class RevocationState private constructor(
val type: RevocationStateType,
private val _date: Date?): Comparable<RevocationState> {
val date: Date
get() {
if (!isSoftRevocation()) {
throw NoSuchElementException("RevocationStateType is not equal to 'softRevoked'. Cannot extract date.")
}
return _date!!
}
private constructor(type: RevocationStateType): this(type, null)
fun isSoftRevocation() = type == RevocationStateType.softRevoked
fun isHardRevocation() = type == RevocationStateType.hardRevoked
fun isNotRevoked() = type == RevocationStateType.notRevoked
companion object {
@JvmStatic
fun notRevoked() = RevocationState(RevocationStateType.notRevoked)
@JvmStatic
fun softRevoked(date: Date) = RevocationState(RevocationStateType.softRevoked, date)
@JvmStatic
fun hardRevoked() = RevocationState(RevocationStateType.hardRevoked)
}
override fun compareTo(other: RevocationState): Int {
return when(type) {
RevocationStateType.notRevoked ->
if (other.isNotRevoked()) 0
else -1
RevocationStateType.softRevoked ->
if (other.isNotRevoked()) 1
// Compare soft dates in reverse
else if (other.isSoftRevocation()) other.date.compareTo(date)
else -1
RevocationStateType.hardRevoked ->
if (other.isHardRevocation()) 0
else 1
else -> throw AssertionError("Unknown type: $type")
}
}
override fun toString(): String {
return buildString {
append(type)
if (isSoftRevocation()) append(" (${DateUtil.formatUTCDate(date)})")
}
}
override fun hashCode(): Int {
return type.hashCode() * 31 + if (isSoftRevocation()) date.hashCode() else 0
}
override fun equals(other: Any?): Boolean {
if (other == null) {
return false
}
if (this === other) {
return true
}
if (other !is RevocationState) {
return false
}
if (type != other.type) {
return false
}
if (isSoftRevocation()) {
return DateUtil.toSecondsPrecision(date).time == DateUtil.toSecondsPrecision(other.date).time
}
return true
}
}

View file

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
enum class RevocationStateType {
/**
* Certificate is not revoked.
*/
notRevoked,
/**
* Certificate is revoked with a soft revocation.
*/
softRevoked,
/**
* Certificate is revoked with a hard revocation.
*/
hardRevoked
}

View file

@ -0,0 +1,401 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
import org.bouncycastle.bcpg.SignatureSubpacketTags.*
/**
* Enumeration of possible subpackets that might be found in the hashed and unhashed area of an OpenPGP signature.
*
* See [RFC4880: Signature Subpacket Specification](https://tools.ietf.org/html/rfc4880#section-5.2.3.1)
*/
enum class SignatureSubpacket(val code: Int) {
/**
* The time the signature was made.
* MUST be present in the hashed area of the signature.
*
* See [Signature Creation Time](https://tools.ietf.org/html/rfc4880#section-5.2.3.4)
*/
signatureCreationTime(2),
/**
* The validity period of the signature. This is the number of seconds
* after the signature creation time that the signature expires. If
* this is not present or has a value of zero, it never expires.
*
* See [Signature Expiration Time](https://tools.ietf.org/html/rfc4880#section-5.2.3.10)
*/
signatureExpirationTime(3),
/**
* Denotes whether the signature is exportable for other users.
*
* See [Exportable Certification](https://tools.ietf.org/html/rfc4880#section-5.2.3.11)
*/
exportableCertification(4),
/**
* Signer asserts that the key is not only valid but also trustworthy at
* the specified level. Level 0 has the same meaning as an ordinary
* validity signature. Level 1 means that the signed key is asserted to
* be a valid, trusted introducer, with the 2nd octet of the body
* specifying the degree of trust. Level 2 means that the signed key is
* asserted to be trusted to issue level 1 trust signatures, i.e., that
* it is a "meta introducer". Generally, a level n trust signature
* asserts that a key is trusted to issue level n-1 trust signatures.
* The trust amount is in a range from 0-255, interpreted such that
* values less than 120 indicate partial trust and values of 120 or
* greater indicate complete trust. Implementations SHOULD emit values
* of 60 for partial trust and 120 for complete trust.
*
* See [Trust Signature](https://tools.ietf.org/html/rfc4880#section-5.2.3.13)
*/
trustSignature(5),
/**
* Used in conjunction with trust Signature packets (of level greater 0) to
* limit the scope of trust that is extended. Only signatures by the
* target key on User IDs that match the regular expression in the body
* of this packet have trust extended by the trust Signature subpacket.
* The regular expression uses the same syntax as the Henry Spencer's
* "almost public domain" regular expression [REGEX] package. A
* description of the syntax is found in Section 8 below.
*
* See [Regular Expression](https://tools.ietf.org/html/rfc4880#section-5.2.3.14)
*/
regularExpression(6),
/**
* Signature's revocability status. The packet body contains a Boolean
* flag indicating whether the signature is revocable. Signatures that
* are not revocable have any later revocation signatures ignored. They
* represent a commitment by the signer that he cannot revoke his
* signature for the life of his key. If this packet is not present,
* the signature is revocable.
*
* See [Revocable](https://tools.ietf.org/html/rfc4880#section-5.2.3.12)
*/
revocable(7),
/**
* The validity period of the key. This is the number of seconds after
* the key creation time that the key expires. If this is not present
* or has a value of zero, the key never expires. This is found only on
* a self-signature.
*
* See [Key Expiration Time](https://tools.ietf.org/html/rfc4880#section-5.2.3.6)
*/
keyExpirationTime(9),
/**
* Placeholder for backwards compatibility.
*/
placeholder(10),
/**
* Symmetric algorithm numbers that indicate which algorithms the keyholder
* prefers to use. The subpackets body is an ordered list of
* octets with the most preferred listed first. It is assumed that only
* algorithms listed are supported by the recipient's software.
* This is only found on a self-signature.
*
* See [Preferred Symmetric Algorithms](https://tools.ietf.org/html/rfc4880#section-5.2.3.7)
*/
preferredSymmetricAlgorithms(11),
/**
* Authorizes the specified key to issue revocation signatures for this
* key. Class octet must have bit 0x80 set. If the bit 0x40 is set,
* then this means that the revocation information is sensitive. Other
* bits are for future expansion to other kinds of authorizations. This
* is found on a self-signature.
*
* If the "sensitive" flag is set, the keyholder feels this subpacket
* contains private trust information that describes a real-world
* sensitive relationship. If this flag is set, implementations SHOULD
* NOT export this signature to other users except in cases where the
* data needs to be available: when the signature is being sent to the
* designated revoker, or when it is accompanied by a revocation
* signature from that revoker. Note that it may be appropriate to
* isolate this subpacket within a separate signature so that it is not
* combined with other subpackets that need to be exported.
*
* See [Revocation Key](https://tools.ietf.org/html/rfc4880#section-5.2.3.15)
*/
revocationKey(12),
/**
* The OpenPGP Key ID of the key issuing the signature.
*
* See [Issuer Key ID](https://tools.ietf.org/html/rfc4880#section-5.2.3.5)
*/
issuerKeyId(16),
/**
* This subpacket describes a "notation" on the signature that the
* issuer wishes to make. The notation has a name and a value, each of
* which are strings of octets. There may be more than one notation in
* a signature. Notations can be used for any extension the issuer of
* the signature cares to make. The "flags" field holds four octets of
* flags.
*
* See [Notation Data](https://tools.ietf.org/html/rfc4880#section-5.2.3.16)
*/
notationData(20),
/**
* Message digest algorithm numbers that indicate which algorithms the
* keyholder prefers to receive. Like the preferred symmetric
* algorithms, the list is ordered.
* This is only found on a self-signature.
*
* See [Preferred Hash Algorithms](https://tools.ietf.org/html/rfc4880#section-5.2.3.8)
*/
preferredHashAlgorithms(21),
/**
* Compression algorithm numbers that indicate which algorithms the
* keyholder prefers to use. Like the preferred symmetric algorithms, the
* list is ordered. If this subpacket is not included, ZIP is preferred.
* A zero denotes that uncompressed data is preferred; the keyholder's
* software might have no compression software in that implementation.
* This is only found on a self-signature.
*
* See [Preferred Compressio Algorithms](https://tools.ietf.org/html/rfc4880#section-5.2.3.9)
*/
preferredCompressionAlgorithms(22),
/**
* This is a list of one-bit flags that indicate preferences that the
* keyholder has about how the key is handled on a key server. All
* undefined flags MUST be zero.
* This is found only on a self-signature.
*
* See [Key Server Preferences](https://tools.ietf.org/html/rfc4880#section-5.2.3.17)
*/
keyServerPreferences(23),
/**
* This is a URI of a key server that the keyholder prefers be used for
* updates. Note that keys with multiple User IDs can have a preferred
* key server for each User ID. Note also that since this is a URI, the
* key server can actually be a copy of the key retrieved by ftp, http,
* finger, etc.
*
* See [Preferred Key Server](https://tools.ietf.org/html/rfc4880#section-5.2.3.18)
*/
preferredKeyServers(24),
/**
* This is a flag in a User ID's self-signature that states whether this
* User ID is the main User ID for this key. It is reasonable for an
* implementation to resolve ambiguities in preferences, etc. by
* referring to the primary User ID. If this flag is absent, its value
* is zero. If more than one User ID in a key is marked as primary, the
* implementation may resolve the ambiguity in any way it sees fit, but
* it is RECOMMENDED that priority be given to the User ID with the most
* recent self-signature.
*
* When appearing on a self-signature on a User ID packet, this
* subpacket applies only to User ID packets. When appearing on a
* self-signature on a User Attribute packet, this subpacket applies
* only to User Attribute packets. That is to say, there are two
* different and independent "primaries" -- one for User IDs, and one
* for User Attributes.
*
* See [Primary User-ID](https://tools.ietf.org/html/rfc4880#section-5.2.3.19)
*/
primaryUserId(25),
/**
* This subpacket contains a URI of a document that describes the policy
* under which the signature was issued.
*
* See [Policy URL](https://tools.ietf.org/html/rfc4880#section-5.2.3.20)
*/
policyUrl(26),
/**
* This subpacket contains a list of binary flags that hold information
* about a key. It is a string of octets, and an implementation MUST
* NOT assume a fixed size. This is so it can grow over time. If a
* list is shorter than an implementation expects, the unstated flags
* are considered to be zero.
*
* See [Key Flags](https://tools.ietf.org/html/rfc4880#section-5.2.3.21)
*/
keyFlags(27),
/**
* This subpacket allows a keyholder to state which User ID is
* responsible for the signing. Many keyholders use a single key for
* different purposes, such as business communications as well as
* personal communications. This subpacket allows such a keyholder to
* state which of their roles is making a signature.
*
* See [Signer's User ID](https://tools.ietf.org/html/rfc4880#section-5.2.3.22)
*/
signerUserId(28),
/**
* This subpacket is used only in key revocation and certification
* revocation signatures. It describes the reason why the key or
* certificate was revoked.
*
* The first octet contains a machine-readable code that denotes the
* reason for the revocation:
*
* 0 - No reason specified (key revocations or cert revocations)
* 1 - Key is superseded (key revocations)
* 2 - Key material has been compromised (key revocations)
* 3 - Key is retired and no longer used (key revocations)
* 32 - User ID information is no longer valid (cert revocations)
* 100-110 - Private Use
*
* See [Reason for Revocation](https://tools.ietf.org/html/rfc4880#section-5.2.3.23)
*/
revocationReason(29),
/**
* The Features subpacket denotes which advanced OpenPGP features a
* user's implementation supports. This is so that as features are
* added to OpenPGP that cannot be backwards-compatible, a user can
* state that they can use that feature. The flags are single bits that
* indicate that a given feature is supported.
*
* This subpacket is similar to a preferences subpacket, and only
* appears in a self-signature.
*
* See [Features](https://tools.ietf.org/html/rfc4880#section-5.2.3.24)
*/
features(30),
/**
* This subpacket identifies a specific target signature to which a
* signature refers. For revocation signatures, this subpacket
* provides explicit designation of which signature is being revoked.
* For a third-party or timestamp signature, this designates what
* signature is signed. All arguments are an identifier of that target
* signature.
*
* The N octets of hash data MUST be the size of the hash of the
* signature. For example, a target signature with a SHA-1 hash MUST
* have 20 octets of hash data.
*
* See [Signature Target](https://tools.ietf.org/html/rfc4880#section-5.2.3.25)
*/
signatureTarget(31),
/**
* This subpacket contains a complete Signature packet body as
* specified in Section 5.2 above. It is useful when one signature
* needs to refer to, or be incorporated in, another signature.
*
* See [Embedded Signature](https://tools.ietf.org/html/rfc4880#section-5.2.3.26)
*/
embeddedSignature(32),
/**
* The OpenPGP Key fingerprint of the key issuing the signature. This
* subpacket SHOULD be included in all signatures. If the version of
* the issuing key is 4 and an Issuer subpacket is also included in the
* signature, the key ID of the Issuer subpacket MUST match the low 64
* bits of the fingerprint.
*
* Note that the length N of the fingerprint for a version 4 key is 20
* octets; for a version 5 key N is 32.
*
* See [Issuer Fingerprint](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.28)
*/
issuerFingerprint(33),
/**
* AEAD algorithm numbers that indicate which AEAD algorithms the
* keyholder prefers to use. The subpackets body is an ordered list of
* octets with the most preferred listed first. It is assumed that only
* algorithms listed are supported by the recipient's software.
* This is only found on a self-signature.
* Note that support for the AEAD Encrypted Data packet in the general
* is indicated by a Feature Flag.
*
* See [Preferred AEAD Algorithms](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.8)
*/
preferredAEADAlgorithms(39),
/**
* The OpenPGP Key fingerprint of the intended recipient primary key.
* If one or more subpackets of this type are included in a signature,
* it SHOULD be considered valid only in an encrypted context, where the
* key it was encrypted to is one of the indicated primary keys, or one
* of their subkeys. This can be used to prevent forwarding a signature
* outside its intended, encrypted context.
*
* Note that the length N of the fingerprint for a version 4 key is 20
* octets; for a version 5 key N is 32.
*
* See [Intended Recipient Fingerprint](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.29)
*/
intendedRecipientFingerprint(35),
/**
* This subpacket MUST only appear as a hashed subpacket of an
* Attestation Key Signature. It has no meaning in any other signature
* type. It is used by the primary key to attest to a set of third-
* party certifications over the associated User ID or User Attribute.
* This enables the holder of an OpenPGP primary key to mark specific
* third-party certifications as re-distributable with the rest of the
* Transferable Public Key (see the "No-modify" flag in "Key Server
* Preferences", above). Implementations MUST include exactly one
* Attested Certification subpacket in any generated Attestation Key
* Signature.
*
* See [Attested Certification](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.30)
*/
attestedCertification(37)
;
companion object {
/**
* Return the [SignatureSubpacket] that corresponds to the provided id.
* If an unmatched code is presented, return null.
*
* @param code id
* @return signature subpacket
*/
@JvmStatic
fun fromCode(code: Int): SignatureSubpacket? {
return values().firstOrNull {
it.code == code
}
}
/**
* Return the [SignatureSubpacket] that corresponds to the provided code.
*
* @param code code
* @return signature subpacket
* @throws NoSuchElementException in case of an unmatched subpacket tag
*/
@JvmStatic
fun requireFromCode(code: Int): SignatureSubpacket {
return fromCode(code) ?:
throw NoSuchElementException("No SignatureSubpacket tag found with code $code")
}
/**
* Convert an array of signature subpacket tags into a list of [SignatureSubpacket SignatureSubpackets].
*
* @param codes array of codes
* @return list of subpackets
*/
@JvmStatic
fun fromCodes(vararg codes: Int): List<SignatureSubpacket> {
return codes.toList().mapNotNull {
fromCode(it)
}
}
}
}

View file

@ -0,0 +1,228 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
import org.bouncycastle.openpgp.PGPSignature
/**
* Enum that enlists all the Signature Types defined in rfc4880 section 5.2.1
* See [PGPSignature] for comparison.
*
* See [rfc4880 §5.2.1. Signature Types](https://tools.ietf.org/html/rfc4880#section-5.11)
*/
enum class SignatureType(val code: Int) {
/**
* Signature of a binary document.
* This means the signer owns it, created it, or certifies that it
* has not been modified.
*/
BINARY_DOCUMENT(0x00),
/**
* Signature of a canonical text document.
* This means the signer owns it, created it, or certifies that it
* has not been modified. The signature is calculated over the text
* data with its line endings converted to {@code <CR><LF>}.
*/
CANONICAL_TEXT_DOCUMENT(0x01),
/**
* Standalone signature.
* This signature is a signature of only its own subpacket contents.
* It is calculated identically to a signature over a zero-length
* binary document. Note that it doesn't make sense to have a V3
* standalone signature.
*/
STANDALONE(0x02),
/**
* Generic certification of a User ID and Public-Key packet.
* The issuer of this certification does not make any particular
* assertion as to how well the certifier has checked that the owner
* of the key is in fact the person described by the User ID.
*/
GENERIC_CERTIFICATION(0x10),
/**
* Persona certification of a User ID and Public-Key packet.
* The issuer of this certification has not done any verification of
* the claim that the owner of this key is the User ID specified.
*/
NO_CERTIFICATION(0x11),
/**
* Casual certification of a User ID and Public-Key packet.
* The issuer of this certification has done some casual
* verification of the claim of identity.
*/
CASUAL_CERTIFICATION(0x12),
/**
* Positive certification of a User ID and Public-Key packet.
* The issuer of this certification has done substantial
* verification of the claim of identity.
*/
POSITIVE_CERTIFICATION(0x13),
/**
* Subkey Binding Signature.
* This signature is a statement by the top-level signing key that
* indicates that it owns the subkey. This signature is calculated
* directly on the primary key and subkey, and not on any User ID or
* other packets. A signature that binds a signing subkey MUST have
* an Embedded Signature subpacket in this binding signature that
* contains a [#PRIMARYKEY_BINDING] signature made by the
* signing subkey on the primary key and subkey.
*/
SUBKEY_BINDING(0x18),
/**
* Primary Key Binding Signature
* This signature is a statement by a signing subkey, indicating
* that it is owned by the primary key and subkey. This signature
* is calculated the same way as a [#SUBKEY_BINDING] signature:
* directly on the primary key and subkey, and not on any User ID or
* other packets.
*/
PRIMARYKEY_BINDING(0x19),
/**
* Signature directly on a key
* This signature is calculated directly on a key. It binds the
* information in the Signature subpackets to the key, and is
* appropriate to be used for subpackets that provide information
* about the key, such as the Revocation Key subpacket. It is also
* appropriate for statements that non-self certifiers want to make
* about the key itself, rather than the binding between a key and a
* name.
*/
DIRECT_KEY(0x1f),
/**
* Key revocation signature
* The signature is calculated directly on the key being revoked. A
* revoked key is not to be used. Only revocation signatures by the
* key being revoked, or by an authorized revocation key, should be
* considered valid revocation signatures.
*/
KEY_REVOCATION(0x20),
/**
* Subkey revocation signature
* The signature is calculated directly on the subkey being revoked.
* A revoked subkey is not to be used. Only revocation signatures
* by the top-level signature key that is bound to this subkey, or
* by an authorized revocation key, should be considered valid
* revocation signatures.
*/
SUBKEY_REVOCATION(0x28),
/**
* Certification revocation signature
* This signature revokes an earlier User ID certification signature
* (signature class 0x10 through 0x13) or signature [#DIRECT_KEY].
* It should be issued by the same key that issued the
* revoked signature or an authorized revocation key. The signature
* is computed over the same data as the certificate that it
* revokes, and should have a later creation date than that
* certificate.
*/
CERTIFICATION_REVOCATION(0x30),
/**
* Timestamp signature.
* This signature is only meaningful for the timestamp contained in
* it.
*/
TIMESTAMP(0x40),
/**
* Third-Party Confirmation signature.
* This signature is a signature over some other OpenPGP Signature
* packet(s). It is analogous to a notary seal on the signed data.
* A third-party signature SHOULD include Signature Target
* subpacket(s) to give easy identification. Note that we really do
* mean SHOULD. There are plausible uses for this (such as a blind
* party that only sees the signature, not the key or source
* document) that cannot include a target subpacket.
*/
THIRD_PARTY_CONFIRMATION(0x50)
;
companion object {
/**
* Convert a numerical id into a [SignatureType].
*
* @param code numeric id
* @return signature type enum
*/
@JvmStatic
fun fromCode(code: Int): SignatureType? {
return values().firstOrNull {
it.code == code
}
}
/**
* Convert a numerical id into a [SignatureType].
*
* @param code numeric id
* @return signature type enum
* @throws NoSuchElementException in case of an unmatched signature type code
*/
@JvmStatic
fun requireFromCode(code: Int): SignatureType {
return fromCode(code) ?:
throw NoSuchElementException("Signature type 0x${Integer.toHexString(code)} appears to be invalid.")
}
/**
* Convert a numerical id into a [SignatureType].
*
* @param code numeric id
* @return signature type enum
* @throws IllegalArgumentException in case of an unmatched signature type code
*/
@JvmStatic
@Deprecated("Deprecated in favor of requireFromCode",
ReplaceWith("requireFromCode"))
fun valueOf(code: Int): SignatureType {
try {
return requireFromCode(code)
} catch (e: NoSuchElementException) {
throw IllegalArgumentException(e.message)
}
}
@JvmStatic
fun isRevocationSignature(signatureType: Int): Boolean {
return isRevocationSignature(valueOf(signatureType))
}
@JvmStatic
fun isRevocationSignature(signatureType: SignatureType): Boolean {
return when (signatureType) {
BINARY_DOCUMENT,
CANONICAL_TEXT_DOCUMENT,
STANDALONE,
GENERIC_CERTIFICATION,
NO_CERTIFICATION,
CASUAL_CERTIFICATION,
POSITIVE_CERTIFICATION,
SUBKEY_BINDING,
PRIMARYKEY_BINDING,
DIRECT_KEY,
TIMESTAMP,
THIRD_PARTY_CONFIRMATION -> false
KEY_REVOCATION,
SUBKEY_REVOCATION,
CERTIFICATION_REVOCATION -> true
else -> throw IllegalArgumentException("Unknown signature type: $signatureType")
}
}
}
}

View file

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
/**
* Enumeration of possible encoding formats of the content of the literal data packet.
*
* See [RFC4880: Literal Data Packet](https://tools.ietf.org/html/rfc4880#section-5.9)
*/
enum class StreamEncoding(val code: Char) {
/**
* The Literal packet contains binary data.
*/
BINARY('b'),
/**
* The Literal packet contains text data, and thus may need line ends converted to local form, or other
* text-mode changes.
*/
TEXT('t'),
/**
* Indication that the implementation believes that the literal data contains UTF-8 text.
*/
UTF8('u'),
/**
* Early versions of PGP also defined a value of 'l' as a 'local' mode for machine-local conversions.
* RFC 1991 [RFC1991] incorrectly stated this local mode flag as '1' (ASCII numeral one).
* Both of these local modes are deprecated.
*/
@Deprecated("LOCAL is deprecated.")
LOCAL('l'),
;
companion object {
/**
* Return the [StreamEncoding] corresponding to the provided code identifier.
* If no matching encoding is found, return null.
*
* @param code identifier
* @return encoding enum
*/
@JvmStatic
fun fromCode(code: Int): StreamEncoding? {
return values().firstOrNull {
it.code == code.toChar()
} ?: if (code == 1) return LOCAL else null
}
/**
* Return the [StreamEncoding] corresponding to the provided code identifier.
* If no matching encoding is found, throw a [NoSuchElementException].
*
* @param code identifier
* @return encoding enum
*
* @throws NoSuchElementException in case of an unmatched identifier
*/
@JvmStatic
fun requireFromCode(code: Int): StreamEncoding {
return fromCode(code) ?:
throw NoSuchElementException("No StreamEncoding found for code $code")
}
}
}

View file

@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
/**
* Enumeration of possible symmetric encryption algorithms.
*
* See [RFC4880: Symmetric-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.2)
*/
enum class SymmetricKeyAlgorithm(val algorithmId: Int) {
/**
* Plaintext or unencrypted data.
*/
NULL (0),
/**
* IDEA is deprecated.
* @deprecated use a different algorithm.
*/
@Deprecated("IDEA is deprecated.")
IDEA (1),
/**
* TripleDES (DES-EDE - 168 bit key derived from 192).
*/
TRIPLE_DES (2),
/**
* CAST5 (128-bit key, as per RFC2144).
*/
CAST5 (3),
/**
* Blowfish (128-bit key, 16 rounds).
*/
BLOWFISH (4),
/**
* Reserved in RFC4880.
* SAFER-SK128 (13 rounds)
*/
SAFER (5),
/**
* Reserved in RFC4880.
* Reserved for DES/SK
*/
DES (6),
/**
* AES with 128-bit key.
*/
AES_128 (7),
/**
* AES with 192-bit key.
*/
AES_192 (8),
/**
* AES with 256-bit key.
*/
AES_256 (9),
/**
* Twofish with 256-bit key.
*/
TWOFISH (10),
/**
* Reserved for Camellia with 128-bit key.
*/
CAMELLIA_128 (11),
/**
* Reserved for Camellia with 192-bit key.
*/
CAMELLIA_192 (12),
/**
* Reserved for Camellia with 256-bit key.
*/
CAMELLIA_256 (13),
;
companion object {
/**
* Return the [SymmetricKeyAlgorithm] enum that corresponds to the provided numeric id.
* If an invalid id is provided, null is returned.
*
* @param id numeric algorithm id
* @return symmetric key algorithm enum
*/
@JvmStatic
fun fromId(id: Int): SymmetricKeyAlgorithm? {
return values().firstOrNull {
it.algorithmId == id
}
}
/**
* Return the [SymmetricKeyAlgorithm] enum that corresponds to the provided numeric id.
* If an invalid id is provided, throw a [NoSuchElementException].
*
* @param id numeric algorithm id
* @return symmetric key algorithm enum
*
* @throws NoSuchElementException if an unmatched id is provided
*/
@JvmStatic
fun requireFromId(id: Int): SymmetricKeyAlgorithm {
return fromId(id) ?:
throw NoSuchElementException("No SymmetricKeyAlgorithm found for id $id")
}
}
}

View file

@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
/**
* Facade class for [org.bouncycastle.bcpg.sig.TrustSignature].
* A trust signature subpacket marks the trustworthiness of a certificate and defines its capabilities to act
* as a trusted introducer.
*/
class Trustworthiness(amount: Int, depth: Int) {
val depth = capDepth(depth)
val amount = capAmount(amount)
/**
* Returns true, if the trust amount is equal to 0.
* This means the key is not trusted.
*
* Otherwise return false
* @return true if untrusted
*/
fun isNotTrusted() = amount == NOT_TRUSTED
/**
* Return true if the certificate is at least marginally trusted.
* That is the case, if the trust amount is greater than 0.
*
* @return true if the cert is at least marginally trusted
*/
fun isMarginallyTrusted() = amount > NOT_TRUSTED
/**
* Return true if the certificate is fully trusted. That is the case if the trust amount is
* greater than or equal to 120.
*
* @return true if the cert is fully trusted
*/
fun isFullyTrusted() = amount >= THRESHOLD_FULLY_CONVINCED
/**
* Return true, if the cert is an introducer. That is the case if the depth is greater 0.
*
* @return true if introducer
*/
fun isIntroducer() = depth >= 1
/**
* Return true, if the certified cert can introduce certificates with trust depth of <pre>otherDepth</pre>.
*
* @param otherDepth other certifications trust depth
* @return true if the cert can introduce the other
*/
fun canIntroduce(otherDepth: Int) = depth > otherDepth
/**
* Return true, if the certified cert can introduce certificates with the given <pre>other</pre> trust depth.
*
* @param other other certificates trust depth
* @return true if the cert can introduce the other
*/
fun canIntroduce(other: Trustworthiness) = canIntroduce(other.depth)
companion object {
const val THRESHOLD_FULLY_CONVINCED = 120 // greater or equal is fully trusted
const val MARGINALLY_CONVINCED = 60 // default value for marginally convinced
const val NOT_TRUSTED = 0 // 0 is not trusted
/**
* This means that we are fully convinced of the trustworthiness of the key.
*
* @return builder
*/
@JvmStatic
fun fullyTrusted() = Builder(THRESHOLD_FULLY_CONVINCED)
/**
* This means that we are marginally (partially) convinced of the trustworthiness of the key.
*
* @return builder
*/
@JvmStatic
fun marginallyTrusted() = Builder(MARGINALLY_CONVINCED)
/**
* This means that we do not trust the key.
* Can be used to overwrite previous trust.
*
* @return builder
*/
@JvmStatic
fun untrusted() = Builder(NOT_TRUSTED)
@JvmStatic
private fun capAmount(amount: Int): Int {
if (amount !in 0..255) {
throw IllegalArgumentException("Trust amount MUST be a value between 0 and 255")
}
return amount
}
@JvmStatic
private fun capDepth(depth: Int): Int {
if (depth !in 0..255) {
throw IllegalArgumentException("Trust depth MUST be a value between 0 and 255")
}
return depth
}
}
class Builder(val amount: Int) {
/**
* The key is a trusted introducer (depth 1).
* Certifications made by this key are considered trustworthy.
*
* @return trust
*/
fun introducer() = Trustworthiness(amount, 1)
/**
* The key is a meta introducer (depth 2).
* This key can introduce trusted introducers of depth 1.
*
* @return trust
*/
fun metaIntroducer() = Trustworthiness(amount, 2)
/**
* The key is a meta introducer of depth <pre>n</pre>.
* This key can introduce meta introducers of depth <pre>n - 1</pre>.
*
* @param n depth
* @return trust
*/
fun metaIntroducerOfDepth(d: Int) = Trustworthiness(amount, d)
}
}

View file

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm.negotiation
import org.pgpainless.algorithm.HashAlgorithm
import org.pgpainless.policy.Policy
/**
* Interface for a class that negotiates [HashAlgorithms][HashAlgorithm].
*
* You can provide your own implementation using custom logic by implementing the
* [negotiateHashAlgorithm(Set)] method.
*/
interface HashAlgorithmNegotiator {
/**
* Pick one [HashAlgorithm] from the ordered set of acceptable algorithms.
*
* @param orderedPrefs hash algorithm preferences
* @return picked algorithms
*/
fun negotiateHashAlgorithm(orderedPrefs: Set<HashAlgorithm>): HashAlgorithm
companion object {
/**
* Return an instance that negotiates [HashAlgorithms][HashAlgorithm] used for non-revocation signatures
* based on the given [Policy].
*
* @param policy algorithm policy
* @return negotiator
*/
@JvmStatic
fun negotiateSignatureHashAlgorithm(policy: Policy): HashAlgorithmNegotiator {
return negotiateByPolicy(policy.signatureHashAlgorithmPolicy)
}
/**
* Return an instance that negotiates [HashAlgorithms][HashAlgorithm] used for revocation signatures
* based on the given [Policy].
*
* @param policy algorithm policy
* @return negotiator
*/
@JvmStatic
fun negotiateRevocationSignatureAlgorithm(policy: Policy): HashAlgorithmNegotiator {
return negotiateByPolicy(policy.revocationSignatureHashAlgorithmPolicy)
}
/**
* Return an instance that negotiates [HashAlgorithms][HashAlgorithm] based on the given
* [Policy.HashAlgorithmPolicy].
*
* @param hashAlgorithmPolicy algorithm policy for hash algorithms
* @return negotiator
*/
@JvmStatic
fun negotiateByPolicy(hashAlgorithmPolicy: Policy.HashAlgorithmPolicy): HashAlgorithmNegotiator {
return object: HashAlgorithmNegotiator {
override fun negotiateHashAlgorithm(orderedPrefs: Set<HashAlgorithm>): HashAlgorithm {
return orderedPrefs.firstOrNull {
hashAlgorithmPolicy.isAcceptable(it)
} ?: hashAlgorithmPolicy.defaultHashAlgorithm()
}
}
}
}
}

View file

@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm.negotiation
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
import org.pgpainless.policy.Policy
import java.lang.IllegalArgumentException
interface SymmetricKeyAlgorithmNegotiator {
/**
* Negotiate a symmetric encryption algorithm.
* If the override is non-null, it will be returned instead of performing an actual negotiation.
* Otherwise, the list of ordered sets containing the preferences of different recipient keys will be
* used to determine a suitable symmetric encryption algorithm.
*
* @param policy algorithm policy
* @param override algorithm override (if not null, return this)
* @param keyPreferences list of preferences per key
* @return negotiated algorithm
*/
fun negotiate(policy: Policy.SymmetricKeyAlgorithmPolicy,
override: SymmetricKeyAlgorithm?,
keyPreferences: List<Set<SymmetricKeyAlgorithm>>): SymmetricKeyAlgorithm
companion object {
@JvmStatic
fun byPopularity(): SymmetricKeyAlgorithmNegotiator {
return object: SymmetricKeyAlgorithmNegotiator {
override fun negotiate(
policy: Policy.SymmetricKeyAlgorithmPolicy,
override: SymmetricKeyAlgorithm?,
keyPreferences: List<Set<SymmetricKeyAlgorithm>>):
SymmetricKeyAlgorithm {
if (override == SymmetricKeyAlgorithm.NULL) {
throw IllegalArgumentException("Algorithm override cannot be NULL (plaintext).")
}
if (override != null) {
return override
}
// algorithm to #occurrences
val supportWeight = buildMap {
keyPreferences.forEach { keyPreference ->
keyPreference.forEach { pref ->
put(pref, getOrDefault(pref, 0) as Int + 1)
}
}
}
// Pivot map and sort by popularity ascending
// score to list(algo)
val byScore = supportWeight.toList()
.map { e -> e.second to e.first }
.groupBy { e -> e.first }
.map { e -> e.key to e.value.map { it.second }.toList() }
.associate { e -> e }
.toSortedMap()
// iterate in reverse over algorithms
for (e in byScore.entries.reversed()) {
val best = policy.selectBest(e.value)
if (best != null) {
return best
}
}
return policy.defaultSymmetricKeyAlgorithm
}
}
}
}
}

View file

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.authentication
import org.bouncycastle.openpgp.PGPPublicKeyRing
class CertificateAuthenticity(val userId: String,
val certificate: PGPPublicKeyRing,
val certificationChains: Map<CertificationChain, Int>,
val targetAmount: Int) {
val totalTrustAmount: Int
get() = certificationChains.values.sum()
/**
* Return the degree of authentication of the binding in percent.
* 100% means full authentication.
* Values smaller than 100% mean partial authentication.
*
* @return authenticity in percent
*/
val authenticityPercentage: Int
get() = targetAmount * 100 / totalTrustAmount
/**
* Return true, if the binding is authenticated to a sufficient degree.
*
* @return true if total gathered evidence outweighs the target trust amount.
*/
val authenticated: Boolean
get() = targetAmount <= totalTrustAmount
fun isAuthenticated() = authenticated
}
/**
* A chain of certifications.
*
* @param trustAmount actual trust amount of the chain
* @param chainLinks links of the chain, starting at the trust-root, ending at the target.
*/
class CertificationChain(
val trustAmount: Int,
val chainLinks: List<ChainLink>) {
}
/**
* A chain link contains a node in the trust chain.
*/
class ChainLink(
val certificate: PGPPublicKeyRing) {
}

View file

@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.authentication;
import org.pgpainless.key.OpenPgpFingerprint
import java.util.*
/**
* Interface for a CA that can authenticate trust-worthy certificates.
* Such a CA might be a fixed list of trustworthy certificates, or a dynamic implementation like the Web-of-Trust.
*
* @see <a href="https://github.com/pgpainless/pgpainless-wot">PGPainless-WOT</a>
* @see <a href="https://sequoia-pgp.gitlab.io/sequoia-wot/">OpenPGP Web of Trust</a>
*/
interface CertificateAuthority {
/**
* Determine the authenticity of the binding between the given fingerprint and the userId.
* In other words, determine, how much evidence can be gathered, that the certificate with the given
* fingerprint really belongs to the user with the given userId.
*
* @param fingerprint fingerprint of the certificate
* @param userId userId
* @param email if true, the userId will be treated as an email address and all user-IDs containing
* the email address will be matched.
* @param referenceTime reference time at which the binding shall be evaluated
* @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly authenticated,
* 60 = partially authenticated...)
* @return information about the authenticity of the binding
*/
fun authenticateBinding(fingerprint: OpenPgpFingerprint,
userId: String,
email: Boolean,
referenceTime: Date,
targetAmount: Int): CertificateAuthenticity;
/**
* Lookup certificates, which carry a trustworthy binding to the given userId.
*
* @param userId userId
* @param email if true, the user-ID will be treated as an email address and all user-IDs containing
* the email address will be matched.
* @param referenceTime reference time at which the binding shall be evaluated
* @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly authenticated,
* 60 = partially authenticated...)
* @return list of identified bindings
*/
fun lookupByUserId(userId: String,
email: Boolean,
referenceTime: Date,
targetAmount: Int): List<CertificateAuthenticity>
/**
* Identify trustworthy bindings for a certificate.
* The result is a list of authenticatable userIds on the certificate.
*
* @param fingerprint fingerprint of the certificate
* @param referenceTime reference time for trust calculations
* @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly authenticated,
* 60 = partially authenticated...)
* @return list of identified bindings
*/
fun identifyByFingerprint(fingerprint: OpenPgpFingerprint,
referenceTime: Date,
targetAmount: Int): List<CertificateAuthenticity>
}

View file

@ -0,0 +1,162 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.key
import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPSecretKey
import org.bouncycastle.util.encoders.Hex
import java.nio.charset.Charset
/**
* Abstract super class of different version OpenPGP fingerprints.
*
*/
abstract class OpenPgpFingerprint : CharSequence, Comparable<OpenPgpFingerprint> {
val fingerprint: String
/**
* Return the version of the fingerprint.
*
* @return version
*/
abstract fun getVersion(): Int
/**
* Return the key id of the OpenPGP public key this {@link OpenPgpFingerprint} belongs to.
* This method can be implemented for V4 and V5 fingerprints.
* V3 key-IDs cannot be derived from the fingerprint, but we don't care, since V3 is deprecated.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-12.2">
* RFC-4880 §12.2: Key IDs and Fingerprints</a>
* @return key id
*/
abstract val keyId: Long
constructor(fingerprint: String) {
val prep = fingerprint.replace(" ", "").trim().uppercase()
if (!isValid(prep)) {
throw IllegalArgumentException("Fingerprint '$fingerprint' does not appear to be a valid OpenPGP V${getVersion()} fingerprint.")
}
this.fingerprint = prep
}
constructor(bytes: ByteArray): this(Hex.toHexString(bytes))
constructor(key: PGPPublicKey): this(key.fingerprint) {
if (key.version != getVersion()) {
throw IllegalArgumentException("Key is not a v${getVersion()} OpenPgp key.")
}
}
constructor(key: PGPSecretKey): this(key.publicKey)
constructor(keys: PGPKeyRing): this(keys.publicKey)
/**
* Check, whether the fingerprint consists of 40 valid hexadecimal characters.
* @param fp fingerprint to check.
* @return true if fingerprint is valid.
*/
protected abstract fun isValid(fingerprint: String): Boolean
override val length: Int
get() = fingerprint.length
override fun get(index: Int) = fingerprint.get(index)
override fun subSequence(startIndex: Int, endIndex: Int) = fingerprint.subSequence(startIndex, endIndex)
override fun compareTo(other: OpenPgpFingerprint): Int {
return fingerprint.compareTo(other.fingerprint)
}
override fun equals(other: Any?): Boolean {
return toString() == other.toString()
}
override fun hashCode(): Int {
return toString().hashCode()
}
override fun toString(): String = fingerprint
abstract fun prettyPrint(): String
companion object {
@JvmStatic
val utf8: Charset = Charset.forName("UTF-8")
/**
* Return the fingerprint of the given key.
* This method automatically matches key versions to fingerprint implementations.
*
* @param key key
* @return fingerprint
*/
@JvmStatic
fun of(key: PGPSecretKey): OpenPgpFingerprint = of(key.publicKey)
/**
* Return the fingerprint of the given key.
* This method automatically matches key versions to fingerprint implementations.
*
* @param key key
* @return fingerprint
*/
@JvmStatic
fun of (key: PGPPublicKey): OpenPgpFingerprint = when(key.version) {
4 -> OpenPgpV4Fingerprint(key)
5 -> OpenPgpV5Fingerprint(key)
6 -> OpenPgpV6Fingerprint(key)
else -> throw IllegalArgumentException("OpenPGP keys of version ${key.version} are not supported.")
}
/**
* Return the fingerprint of the primary key of the given key ring.
* This method automatically matches key versions to fingerprint implementations.
*
* @param ring key ring
* @return fingerprint
*/
@JvmStatic
fun of (keys: PGPKeyRing): OpenPgpFingerprint = of(keys.publicKey)
/**
* Try to parse an {@link OpenPgpFingerprint} from the given fingerprint string.
* If the trimmed fingerprint without whitespace is 64 characters long, it is either a v5 or v6 fingerprint.
* In this case, we return a {@link _64DigitFingerprint}. Since this is ambiguous, it is generally recommended
* to know the version of the key beforehand.
*
* @param fingerprint fingerprint
* @return parsed fingerprint
* @deprecated Use the constructor methods of the versioned fingerprint subclasses instead.
*/
@JvmStatic
@Deprecated("Use the constructor methods of the versioned fingerprint subclasses instead.")
fun parse(fingerprint: String): OpenPgpFingerprint {
val prep = fingerprint.replace(" ", "").trim().uppercase()
if (prep.matches("^[0-9A-F]{40}$".toRegex())) {
return OpenPgpV4Fingerprint(prep)
}
if (prep.matches("^[0-9A-F]{64}$".toRegex())) {
// Might be v5 or v6 :/
return _64DigitFingerprint(prep)
}
throw IllegalArgumentException("Fingerprint does not appear to match any known fingerprint pattern.")
}
/**
* Parse a binary OpenPGP fingerprint into an {@link OpenPgpFingerprint} object.
*
* @param binaryFingerprint binary representation of the fingerprint
* @return parsed fingerprint
* @deprecated use the parse() methods of the versioned fingerprint subclasses instead.
*/
@JvmStatic
@Deprecated("use the parse() methods of the versioned fingerprint subclasses instead.")
fun parseFromBinary(binaryFingerprint: ByteArray): OpenPgpFingerprint =
parse(Hex.toHexString(binaryFingerprint))
}
}

View file

@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.key
import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPSecretKey
import org.bouncycastle.util.encoders.Hex
import java.net.URI
import java.nio.Buffer
import java.nio.ByteBuffer
import java.nio.charset.Charset
class OpenPgpV4Fingerprint: OpenPgpFingerprint {
constructor(fingerprint: String): super(fingerprint)
constructor(bytes: ByteArray): super(bytes)
constructor(key: PGPPublicKey): super(key)
constructor(key: PGPSecretKey): super(key)
constructor(keys: PGPKeyRing): super(keys)
override fun getVersion() = 4
override val keyId: Long
get() {
val bytes = Hex.decode(toString().toByteArray(Charset.forName("UTF-8")))
val buf = ByteBuffer.wrap(bytes)
// The key id is the right-most 8 bytes (conveniently a long)
// We have to cast here in order to be compatible with java 8
// https://github.com/eclipse/jetty.project/issues/3244
(buf as Buffer).position(12) // 20 - 8 bytes = offset 12
return buf.getLong()
}
override fun isValid(fingerprint: String): Boolean {
return fingerprint.matches("^[0-9A-F]{40}$".toRegex())
}
fun toUri(): URI = URI(SCHEME, toString(), null)
override fun prettyPrint(): String {
return buildString {
for (i in 0..4) {
append(fingerprint, i * 4, (i + 1) * 4).append(' ')
}
append(' ')
for (i in 5 .. 8) {
append(fingerprint, i * 4, (i + 1) * 4).append(' ')
}
append(fingerprint, 36, 40)
}
}
companion object {
@JvmStatic
val SCHEME = "openpgp4fpr"
@JvmStatic
fun fromUri(uri: URI): OpenPgpV4Fingerprint {
if (SCHEME != uri.scheme) {
throw IllegalArgumentException("URI scheme MUST equal '$SCHEME'.")
}
return OpenPgpV4Fingerprint(uri.schemeSpecificPart)
}
}
}

View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.key
import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPSecretKey
/**
* This class represents a hex encoded uppercase OpenPGP v5 fingerprint.
*/
class OpenPgpV5Fingerprint: _64DigitFingerprint {
constructor(fingerprint: String): super(fingerprint)
constructor(key: PGPPublicKey): super(key)
constructor(key: PGPSecretKey): super(key)
constructor(keys: PGPKeyRing): super(keys)
constructor(bytes: ByteArray): super(bytes)
override fun getVersion(): Int {
return 5
}
}

View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.key
import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPSecretKey
/**
* This class represents a hex encoded, uppercase OpenPGP v6 fingerprint.
*/
class OpenPgpV6Fingerprint: _64DigitFingerprint {
constructor(fingerprint: String): super(fingerprint)
constructor(key: PGPPublicKey): super(key)
constructor(key: PGPSecretKey): super(key)
constructor(keys: PGPKeyRing): super(keys)
constructor(bytes: ByteArray): super(bytes)
override fun getVersion(): Int {
return 6
}
}

View file

@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.key
import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPPublicKey
import org.pgpainless.key.util.KeyIdUtil
/**
* Tuple class used to identify a subkey by fingerprints of the primary key of the subkeys key ring,
* as well as the subkeys fingerprint.
*/
class SubkeyIdentifier(
val primaryKeyFingerprint: OpenPgpFingerprint,
val subkeyFingerprint: OpenPgpFingerprint) {
constructor(fingerprint: OpenPgpFingerprint): this(fingerprint, fingerprint)
constructor(keys: PGPKeyRing): this(keys.publicKey)
constructor(key: PGPPublicKey): this(OpenPgpFingerprint.of(key))
constructor(keys: PGPKeyRing, keyId: Long): this(
OpenPgpFingerprint.of(keys.publicKey),
OpenPgpFingerprint.of(keys.getPublicKey(keyId) ?:
throw NoSuchElementException("OpenPGP key does not contain subkey ${KeyIdUtil.formatKeyId(keyId)}")))
constructor(keys: PGPKeyRing, subkeyFingerprint: OpenPgpFingerprint): this(OpenPgpFingerprint.of(keys), subkeyFingerprint)
val keyId = subkeyFingerprint.keyId
val fingerprint = subkeyFingerprint
val subkeyId = subkeyFingerprint.keyId
val primaryKeyId = primaryKeyFingerprint.keyId
override fun equals(other: Any?): Boolean {
if (other == null) {
return false
}
if (this === other) {
return true
}
if (other !is SubkeyIdentifier) {
return false
}
return primaryKeyFingerprint.equals(other.primaryKeyFingerprint) && subkeyFingerprint.equals(other.subkeyFingerprint)
}
override fun hashCode(): Int {
return primaryKeyFingerprint.hashCode() + 31 * subkeyFingerprint.hashCode()
}
override fun toString(): String = "$subkeyFingerprint $primaryKeyFingerprint"
}

View file

@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.key;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPKeyRing;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.encoders.Hex;
/**
* This class represents a hex encoded, upper case OpenPGP v5 or v6 fingerprint.
* Since both fingerprints use the same format, this class is used when parsing the fingerprint without knowing the
* key version.
*/
open class _64DigitFingerprint: OpenPgpFingerprint {
/**
* Create an {@link _64DigitFingerprint}.
*
* @param fingerprint uppercase hexadecimal fingerprint of length 64
*/
constructor(fingerprint: String): super(fingerprint)
constructor(bytes: ByteArray): super(bytes)
constructor(key: PGPPublicKey): super(key)
constructor(key: PGPSecretKey): super(key)
constructor(keys: PGPKeyRing): super(keys)
override val keyId: Long
get() {
val bytes = Hex.decode(fingerprint.toByteArray(Charset.forName("UTF-8")))
val buf = ByteBuffer.wrap(bytes);
// The key id is the left-most 8 bytes (conveniently a long).
// We have to cast here in order to be compatible with java 8
// https://github.com/eclipse/jetty.project/issues/3244
(buf as Buffer).position(0);
return buf.getLong()
}
override fun getVersion(): Int {
return -1 // might be v5 or v6
}
override fun isValid(fingerprint: String): Boolean {
return fingerprint.matches(("^[0-9A-F]{64}$".toRegex()))
}
override fun toString(): String {
return super.toString()
}
override fun prettyPrint(): String {
return buildString {
for (i in 0 until 4) {
append(fingerprint, i * 8, (i + 1) * 8).append(' ');
}
append(' ');
for (i in 4 until 7) {
append(fingerprint, i * 8, (i + 1) * 8).append(' ');
}
append(fingerprint, 56, 64);
}
}
}

View file

@ -0,0 +1,359 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.policy
import org.pgpainless.algorithm.*
import org.pgpainless.util.DateUtil
import org.pgpainless.util.NotationRegistry
import java.util.*
class Policy(
var signatureHashAlgorithmPolicy: HashAlgorithmPolicy,
var revocationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy,
var symmetricKeyEncryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy,
var symmetricKeyDecryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy,
var compressionAlgorithmPolicy: CompressionAlgorithmPolicy,
var publicKeyAlgorithmPolicy: PublicKeyAlgorithmPolicy,
var notationRegistry: NotationRegistry) {
constructor(): this(
HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(),
HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(),
SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022(),
SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022(),
CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy(),
PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy(),
NotationRegistry())
var keyGenerationAlgorithmSuite = AlgorithmSuite.defaultAlgorithmSuite
var signerUserIdValidationLevel = SignerUserIdValidationLevel.DISABLED
var enableKeyParameterValidation = false
fun isEnableKeyParameterValidation() = enableKeyParameterValidation
/**
* Create a HashAlgorithmPolicy which accepts all [HashAlgorithms][HashAlgorithm] from the
* given map, if the queried usage date is BEFORE the respective termination date.
* A termination date value of <pre>null</pre> means no termination, resulting in the algorithm being
* acceptable, regardless of usage date.
*
* @param defaultHashAlgorithm default hash algorithm
* @param algorithmTerminationDates map of acceptable algorithms and their termination dates
*/
class HashAlgorithmPolicy(
val defaultHashAlgorithm: HashAlgorithm,
val acceptableHashAlgorithmsAndTerminationDates: Map<HashAlgorithm, Date?>) {
/**
* Create a [HashAlgorithmPolicy] which accepts all [HashAlgorithms][HashAlgorithm] listed in
* the given list, regardless of usage date.
*
* @param defaultHashAlgorithm default hash algorithm (e.g. used as fallback if negotiation fails)
* @param acceptableHashAlgorithms list of acceptable hash algorithms
*/
constructor(defaultHashAlgorithm: HashAlgorithm, acceptableHashAlgorithms: List<HashAlgorithm>) :
this(defaultHashAlgorithm, acceptableHashAlgorithms.associateWith { null })
fun isAcceptable(hashAlgorithm: HashAlgorithm) = isAcceptable(hashAlgorithm, Date())
/**
* Return true, if the given algorithm is acceptable for the given usage date.
*
* @param hashAlgorithm algorithm
* @param referenceTime usage date (e.g. signature creation time)
*
* @return acceptance
*/
fun isAcceptable(hashAlgorithm: HashAlgorithm, referenceTime: Date): Boolean {
if (!acceptableHashAlgorithmsAndTerminationDates.containsKey(hashAlgorithm))
return false
val terminationDate = acceptableHashAlgorithmsAndTerminationDates[hashAlgorithm] ?: return true
return terminationDate > referenceTime
}
fun isAcceptable(algorithmId: Int) = isAcceptable(algorithmId, Date())
fun isAcceptable(algorithmId: Int, referenceTime: Date): Boolean {
val algorithm = HashAlgorithm.fromId(algorithmId) ?: return false
return isAcceptable(algorithm, referenceTime)
}
fun defaultHashAlgorithm() = defaultHashAlgorithm
companion object {
@JvmStatic
fun smartSignatureHashAlgorithmPolicy() = HashAlgorithmPolicy(
HashAlgorithm.SHA512, buildMap {
put(HashAlgorithm.SHA3_512, null)
put(HashAlgorithm.SHA3_256, null)
put(HashAlgorithm.SHA512, null)
put(HashAlgorithm.SHA384, null)
put(HashAlgorithm.SHA256, null)
put(HashAlgorithm.SHA224, null)
put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC"))
put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC"))
put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC"))
})
/**
* [HashAlgorithmPolicy] which only accepts signatures made using algorithms which are acceptable
* according to 2022 standards.
*
* Particularly this policy only accepts algorithms from the SHA2 and SHA3 families.
*
* @return static signature algorithm policy
*/
@JvmStatic
fun static2022SignatureHashAlgorithmPolicy() = HashAlgorithmPolicy(
HashAlgorithm.SHA512,
listOf(
HashAlgorithm.SHA3_512,
HashAlgorithm.SHA3_256,
HashAlgorithm.SHA512,
HashAlgorithm.SHA384,
HashAlgorithm.SHA256,
HashAlgorithm.SHA224)
)
/**
* Hash algorithm policy for revocation signatures, which accepts SHA1 and SHA2 algorithms, as well as RIPEMD160.
*
* @return static revocation signature hash algorithm policy
*/
@JvmStatic
fun static2022RevocationSignatureHashAlgorithmPolicy() = HashAlgorithmPolicy(
HashAlgorithm.SHA512,
listOf(
HashAlgorithm.SHA3_512,
HashAlgorithm.SHA3_256,
HashAlgorithm.SHA512,
HashAlgorithm.SHA384,
HashAlgorithm.SHA256,
HashAlgorithm.SHA224,
HashAlgorithm.SHA1,
HashAlgorithm.RIPEMD160
)
)
}
}
class SymmetricKeyAlgorithmPolicy(
val defaultSymmetricKeyAlgorithm: SymmetricKeyAlgorithm,
val acceptableSymmetricKeyAlgorithms: List<SymmetricKeyAlgorithm>) {
fun isAcceptable(algorithm: SymmetricKeyAlgorithm) = acceptableSymmetricKeyAlgorithms.contains(algorithm)
fun isAcceptable(algorithmId: Int): Boolean {
val algorithm = SymmetricKeyAlgorithm.fromId(algorithmId) ?: return false
return isAcceptable(algorithm)
}
fun selectBest(options: List<SymmetricKeyAlgorithm>): SymmetricKeyAlgorithm? {
for (acceptable in acceptableSymmetricKeyAlgorithms) {
if (options.contains(acceptable)) {
return acceptable
}
}
return null
}
companion object {
/**
* The default symmetric encryption algorithm policy of PGPainless.
*
* @return default symmetric encryption algorithm policy
* @deprecated not expressive - will be removed in a future release
*/
@JvmStatic
@Deprecated(
"Not expressive - will be removed in a future release",
ReplaceWith("symmetricKeyEncryptionPolicy2022"))
fun defaultSymmetricKeyEncryptionAlgorithmPolicy() = symmetricKeyEncryptionPolicy2022()
/**
* Policy for symmetric encryption algorithms in the context of message production (encryption).
* This suite contains algorithms that are deemed safe to use in 2022.
*
* @return 2022 symmetric key encryption algorithm policy
*/
@JvmStatic
fun symmetricKeyEncryptionPolicy2022() = SymmetricKeyAlgorithmPolicy(
SymmetricKeyAlgorithm.AES_128,
// Reject: Unencrypted, IDEA, TripleDES, CAST5, Blowfish
listOf(
SymmetricKeyAlgorithm.AES_256,
SymmetricKeyAlgorithm.AES_192,
SymmetricKeyAlgorithm.AES_128,
SymmetricKeyAlgorithm.TWOFISH,
SymmetricKeyAlgorithm.CAMELLIA_256,
SymmetricKeyAlgorithm.CAMELLIA_192,
SymmetricKeyAlgorithm.CAMELLIA_128
))
/**
* The default symmetric decryption algorithm policy of PGPainless.
*
* @return default symmetric decryption algorithm policy
* @deprecated not expressive - will be removed in a future update
*/
@JvmStatic
@Deprecated("not expressive - will be removed in a future update",
ReplaceWith("symmetricKeyDecryptionPolicy2022()"))
fun defaultSymmetricKeyDecryptionAlgorithmPolicy() = symmetricKeyDecryptionPolicy2022()
/**
* Policy for symmetric key encryption algorithms in the context of message consumption (decryption).
* This suite contains algorithms that are deemed safe to use in 2022.
*
* @return 2022 symmetric key decryption algorithm policy
*/
@JvmStatic
fun symmetricKeyDecryptionPolicy2022() = SymmetricKeyAlgorithmPolicy(
SymmetricKeyAlgorithm.AES_128,
// Reject: Unencrypted, IDEA, TripleDES, Blowfish
listOf(
SymmetricKeyAlgorithm.AES_256,
SymmetricKeyAlgorithm.AES_192,
SymmetricKeyAlgorithm.AES_128,
SymmetricKeyAlgorithm.TWOFISH,
SymmetricKeyAlgorithm.CAMELLIA_256,
SymmetricKeyAlgorithm.CAMELLIA_192,
SymmetricKeyAlgorithm.CAMELLIA_128,
SymmetricKeyAlgorithm.CAST5
))
}
}
class CompressionAlgorithmPolicy(
val defaultCompressionAlgorithm: CompressionAlgorithm,
val acceptableCompressionAlgorithms: List<CompressionAlgorithm>) {
fun isAcceptable(algorithm: CompressionAlgorithm) = acceptableCompressionAlgorithms.contains(algorithm)
fun isAcceptable(algorithmId: Int): Boolean {
val algorithm = CompressionAlgorithm.fromId(algorithmId) ?: return false
return isAcceptable(algorithm)
}
fun defaultCompressionAlgorithm() = defaultCompressionAlgorithm
companion object {
/**
* Default {@link CompressionAlgorithmPolicy} of PGPainless.
* The default compression algorithm policy accepts any compression algorithm.
*
* @return default algorithm policy
* @deprecated not expressive - might be removed in a future release
*/
@JvmStatic
@Deprecated("not expressive - might be removed in a future release",
ReplaceWith("anyCompressionAlgorithmPolicy()"))
fun defaultCompressionAlgorithmPolicy() = anyCompressionAlgorithmPolicy()
/**
* Policy that accepts any known compression algorithm and offers [CompressionAlgorithm.ZIP] as
* default algorithm.
*
* @return compression algorithm policy
*/
@JvmStatic
fun anyCompressionAlgorithmPolicy() = CompressionAlgorithmPolicy(
CompressionAlgorithm.ZIP,
listOf(CompressionAlgorithm.UNCOMPRESSED,
CompressionAlgorithm.ZIP,
CompressionAlgorithm.BZIP2,
CompressionAlgorithm.ZLIB))
}
}
class PublicKeyAlgorithmPolicy(private val algorithmStrengths: Map<PublicKeyAlgorithm, Int>) {
fun isAcceptable(algorithm: PublicKeyAlgorithm, bitStrength: Int): Boolean {
return bitStrength >= (algorithmStrengths[algorithm] ?: return false)
}
fun isAcceptable(algorithmId: Int, bitStrength: Int): Boolean {
val algorithm = PublicKeyAlgorithm.fromId(algorithmId) ?: return false
return isAcceptable(algorithm, bitStrength)
}
companion object {
/**
* Return PGPainless' default public key algorithm policy.
* This policy is based upon recommendations made by the German Federal Office for Information Security (BSI).
*
* @return default algorithm policy
* @deprecated not expressive - might be removed in a future release
*/
@JvmStatic
@Deprecated("not expressive - might be removed in a future release",
ReplaceWith("bsi2021PublicKeyAlgorithmPolicy()"))
fun defaultPublicKeyAlgorithmPolicy() = bsi2021PublicKeyAlgorithmPolicy()
/**
* This policy is based upon recommendations made by the German Federal Office for Information Security (BSI).
*
* Basically this policy requires keys based on elliptic curves to have a bit strength of at least 250,
* and keys based on prime number factorization / discrete logarithm problems to have a strength of at least 2000 bits.
*
* @see <a href="https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-1.pdf">BSI - Technical Guideline - Cryptographic Mechanisms: Recommendations and Key Lengths (2021-01)</a>
* @see <a href="https://www.keylength.com/">BlueKrypt | Cryptographic Key Length Recommendation</a>
*
* @return default algorithm policy
*/
@JvmStatic
fun bsi2021PublicKeyAlgorithmPolicy() = PublicKeyAlgorithmPolicy(buildMap {
// §5.4.1
put(PublicKeyAlgorithm.RSA_GENERAL, 2000)
put(PublicKeyAlgorithm.RSA_SIGN, 2000)
put(PublicKeyAlgorithm.RSA_ENCRYPT, 2000)
// Note: ElGamal is not mentioned in the BSI document.
// We assume that the requirements are similar to other DH algorithms
put(PublicKeyAlgorithm.ELGAMAL_ENCRYPT, 2000)
put(PublicKeyAlgorithm.ELGAMAL_GENERAL, 2000)
// §5.4.2
put(PublicKeyAlgorithm.DSA, 2000)
// §5.4.3
put(PublicKeyAlgorithm.ECDSA, 250)
// Note: EdDSA is not mentioned in the BSI document.
// We assume that the requirements are similar to other EC algorithms.
put(PublicKeyAlgorithm.EDDSA, 250)
// §7.2.1
put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000)
// §7.2.2
put(PublicKeyAlgorithm.ECDH, 250)
})
}
}
enum class SignerUserIdValidationLevel {
/**
* PGPainless will verify {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets in signatures strictly.
* This means, that signatures with Signer's User-ID subpackets containing a value that does not match the signer key's
* user-id exactly, will be rejected.
* E.g. Signer's user-id "alice@pgpainless.org", User-ID: "Alice &lt;alice@pgpainless.org&gt;" does not
* match exactly and is therefore rejected.
*/
STRICT,
/**
* PGPainless will ignore {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets on signature.
*/
DISABLED
}
companion object {
@Volatile
private var INSTANCE: Policy? = null
@JvmStatic
fun getInstance() = INSTANCE ?: synchronized(this) {
INSTANCE ?: Policy().also { INSTANCE = it }
}
}
}