Kolin conversion: Some OpenPgpFingerprint classes

This commit is contained in:
Paul Schaub 2023-08-06 15:56:44 +02:00
parent 84e554fc0d
commit 344421a0f4
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
6 changed files with 297 additions and 457 deletions

View File

@ -1,189 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.key;
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;
/**
* Abstract super class of different version OpenPGP fingerprints.
*
*/
public abstract class OpenPgpFingerprint implements CharSequence, Comparable<OpenPgpFingerprint> {
@SuppressWarnings("CharsetObjectCanBeUsed")
protected static final Charset utf8 = Charset.forName("UTF-8");
protected final String fingerprint;
/**
* Return the fingerprint of the given key.
* This method automatically matches key versions to fingerprint implementations.
*
* @param key key
* @return fingerprint
*/
public static OpenPgpFingerprint of(PGPSecretKey key) {
return of(key.getPublicKey());
}
/**
* Return the fingerprint of the given key.
* This method automatically matches key versions to fingerprint implementations.
*
* @param key key
* @return fingerprint
*/
public static OpenPgpFingerprint of(PGPPublicKey key) {
if (key.getVersion() == 4) {
return new OpenPgpV4Fingerprint(key);
}
if (key.getVersion() == 5) {
return new OpenPgpV5Fingerprint(key);
}
if (key.getVersion() == 6) {
return new OpenPgpV6Fingerprint(key);
}
throw new IllegalArgumentException("OpenPGP keys of version " + key.getVersion() + " 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
*/
public static OpenPgpFingerprint of(PGPKeyRing ring) {
return of(ring.getPublicKey());
}
/**
* 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.
*/
@Deprecated
public static OpenPgpFingerprint parse(String fingerprint) {
String fp = fingerprint.replace(" ", "").trim().toUpperCase();
if (fp.matches("^[0-9A-F]{40}$")) {
return new OpenPgpV4Fingerprint(fp);
}
if (fp.matches("^[0-9A-F]{64}$")) {
// Might be v5 or v6 :/
return new _64DigitFingerprint(fp);
}
throw new IllegalArgumentException("Fingerprint does not appear to match any known fingerprint patterns.");
}
/**
* 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.
*/
@Deprecated
public static OpenPgpFingerprint parseFromBinary(byte[] binaryFingerprint) {
String hex = Hex.toHexString(binaryFingerprint).toUpperCase();
return parse(hex);
}
public OpenPgpFingerprint(String fingerprint) {
String fp = fingerprint.replace(" ", "").trim().toUpperCase();
if (!isValid(fp)) {
throw new IllegalArgumentException(
String.format("Fingerprint '%s' does not appear to be a valid OpenPGP V%d fingerprint.", fingerprint, getVersion())
);
}
this.fingerprint = fp;
}
public OpenPgpFingerprint(@Nonnull byte[] bytes) {
this(new String(bytes, utf8));
}
public OpenPgpFingerprint(PGPPublicKey key) {
this(Hex.encode(key.getFingerprint()));
if (key.getVersion() != getVersion()) {
throw new IllegalArgumentException(String.format("Key is not a v%d OpenPgp key.", getVersion()));
}
}
public OpenPgpFingerprint(@Nonnull PGPPublicKeyRing ring) {
this(ring.getPublicKey());
}
public OpenPgpFingerprint(@Nonnull PGPSecretKeyRing ring) {
this(ring.getPublicKey());
}
public OpenPgpFingerprint(@Nonnull PGPKeyRing ring) {
this(ring.getPublicKey());
}
/**
* Return the version of the fingerprint.
*
* @return version
*/
public abstract int getVersion();
/**
* Check, whether the fingerprint consists of 40 valid hexadecimal characters.
* @param fp fingerprint to check.
* @return true if fingerprint is valid.
*/
protected abstract boolean isValid(@Nonnull String fp);
/**
* 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
*/
public abstract long getKeyId();
@Override
public int length() {
return fingerprint.length();
}
@Override
public char charAt(int i) {
return fingerprint.charAt(i);
}
@Override
public CharSequence subSequence(int i, int i1) {
return fingerprint.subSequence(i, i1);
}
@Override
@Nonnull
public String toString() {
return fingerprint;
}
/**
* Return a pretty printed representation of the fingerprint.
*
* @return pretty printed fingerprint
*/
public abstract String prettyPrint();
}

View File

@ -0,0 +1,158 @@
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

@ -1,149 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.key;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
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, uppercase OpenPGP v4 fingerprint.
*/
public class OpenPgpV4Fingerprint extends OpenPgpFingerprint {
public static final String SCHEME = "openpgp4fpr";
/**
* Create an {@link OpenPgpV4Fingerprint}.
*
* @param fingerprint uppercase hexadecimal fingerprint of length 40
*/
public OpenPgpV4Fingerprint(@Nonnull String fingerprint) {
super(fingerprint);
}
public OpenPgpV4Fingerprint(@Nonnull byte[] bytes) {
super(Hex.encode(bytes));
}
public OpenPgpV4Fingerprint(@Nonnull PGPPublicKey key) {
super(key);
}
public OpenPgpV4Fingerprint(@Nonnull PGPSecretKey key) {
this(key.getPublicKey());
}
public OpenPgpV4Fingerprint(@Nonnull PGPPublicKeyRing ring) {
super(ring);
}
public OpenPgpV4Fingerprint(@Nonnull PGPSecretKeyRing ring) {
super(ring);
}
public OpenPgpV4Fingerprint(@Nonnull PGPKeyRing ring) {
super(ring);
}
@Override
public int getVersion() {
return 4;
}
@Override
protected boolean isValid(@Nonnull String fp) {
return fp.matches("^[0-9A-F]{40}$");
}
@Override
public long getKeyId() {
byte[] bytes = Hex.decode(toString().getBytes(utf8));
ByteBuffer 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
((Buffer) buf).position(12); // 20 - 8 bytes = offset 12
return buf.getLong();
}
@Override
public String prettyPrint() {
String fp = toString();
StringBuilder pretty = new StringBuilder();
for (int i = 0; i < 5; i++) {
pretty.append(fp, i * 4, (i + 1) * 4).append(' ');
}
pretty.append(' ');
for (int i = 5; i < 9; i++) {
pretty.append(fp, i * 4, (i + 1) * 4).append(' ');
}
pretty.append(fp, 36, 40);
return pretty.toString();
}
@Override
public boolean equals(Object other) {
if (other == null) {
return false;
}
if (!(other instanceof CharSequence)) {
return false;
}
return this.toString().equals(other.toString());
}
@Override
public int hashCode() {
return toString().hashCode();
}
/**
* Return the fingerprint as an openpgp4fpr {@link URI}.
* An example would be 'openpgp4fpr:7F9116FEA90A5983936C7CFAA027DB2F3E1E118A'.
*
* @return openpgp4fpr fingerprint uri
* @see <a href="https://metacode.biz/openpgp/openpgp4fpr">openpgp4fpr URI scheme</a>
*/
public URI toUri() {
try {
return new URI(OpenPgpV4Fingerprint.SCHEME, toString(), null);
} catch (URISyntaxException e) {
throw new AssertionError(e);
}
}
/**
* Convert an openpgp4fpr URI to an {@link OpenPgpV4Fingerprint}.
*
* @param uri {@link URI} with scheme 'openpgp4fpr'
* @return fingerprint parsed from the uri
* @see <a href="https://metacode.biz/openpgp/openpgp4fpr">openpgp4fpr URI scheme</a>
*/
public static OpenPgpV4Fingerprint fromUri(URI uri) {
if (!SCHEME.equals(uri.getScheme())) {
throw new IllegalArgumentException("URI scheme MUST equal '" + SCHEME + "'");
}
return new OpenPgpV4Fingerprint(uri.getSchemeSpecificPart());
}
@Override
public int compareTo(@Nonnull OpenPgpFingerprint openPgpFingerprint) {
return toString().compareTo(openPgpFingerprint.toString());
}
}

View File

@ -0,0 +1,65 @@
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

@ -1,119 +0,0 @@
// 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 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.
*/
public class _64DigitFingerprint extends OpenPgpFingerprint {
/**
* Create an {@link _64DigitFingerprint}.
*
* @param fingerprint uppercase hexadecimal fingerprint of length 64
*/
protected _64DigitFingerprint(@Nonnull String fingerprint) {
super(fingerprint);
}
protected _64DigitFingerprint(@Nonnull byte[] bytes) {
super(Hex.encode(bytes));
}
protected _64DigitFingerprint(@Nonnull PGPPublicKey key) {
super(key);
}
protected _64DigitFingerprint(@Nonnull PGPSecretKey key) {
this(key.getPublicKey());
}
protected _64DigitFingerprint(@Nonnull PGPPublicKeyRing ring) {
super(ring);
}
protected _64DigitFingerprint(@Nonnull PGPSecretKeyRing ring) {
super(ring);
}
protected _64DigitFingerprint(@Nonnull PGPKeyRing ring) {
super(ring);
}
@Override
public int getVersion() {
return -1; // might be v5 or v6
}
@Override
protected boolean isValid(@Nonnull String fp) {
return fp.matches("^[0-9A-F]{64}$");
}
@Override
public long getKeyId() {
byte[] bytes = Hex.decode(toString().getBytes(utf8));
ByteBuffer 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
((Buffer) buf).position(0);
return buf.getLong();
}
@Override
public String prettyPrint() {
String fp = toString();
StringBuilder pretty = new StringBuilder();
for (int i = 0; i < 4; i++) {
pretty.append(fp, i * 8, (i + 1) * 8).append(' ');
}
pretty.append(' ');
for (int i = 4; i < 7; i++) {
pretty.append(fp, i * 8, (i + 1) * 8).append(' ');
}
pretty.append(fp, 56, 64);
return pretty.toString();
}
@Override
public boolean equals(Object other) {
if (other == null) {
return false;
}
if (!(other instanceof CharSequence)) {
return false;
}
return this.toString().equals(other.toString());
}
@Override
public int hashCode() {
return toString().hashCode();
}
@Override
public int compareTo(OpenPgpFingerprint openPgpFingerprint) {
return toString().compareTo(openPgpFingerprint.toString());
}
}

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);
}
}
}