From 8f473b513f9ede165f6f0c81dff311492b7d35a5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Mar 2022 12:01:12 +0100 Subject: [PATCH] Add support for OpenPGP v5 fingerprints. Obviously we need support for key.getFingerprint() in BC, but once that is there, this should magically start working. --- .../pgpainless/key/OpenPgpFingerprint.java | 3 + .../pgpainless/key/OpenPgpV4Fingerprint.java | 80 ++++++------ .../pgpainless/key/OpenPgpV5Fingerprint.java | 117 ++++++++++++++++++ .../key/OpenPgpV5FingerprintTest.java | 25 ++++ 4 files changed, 185 insertions(+), 40 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java index af0051c8..b00e4d6f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java @@ -33,6 +33,9 @@ public abstract class OpenPgpFingerprint implements CharSequence, Comparable - * XEP-0373 ยง4.1: The OpenPGP Public-Key Data Node about how to obtain the fingerprint - * @param fingerprint hexadecimal representation of the fingerprint. + * + * @param fingerprint uppercase hexadecimal fingerprint of length 40 */ public OpenPgpV4Fingerprint(@Nonnull String fingerprint) { super(fingerprint); } - @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); - - // 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); - - 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(); - } - public OpenPgpV4Fingerprint(@Nonnull byte[] bytes) { super(Hex.encode(bytes)); } @@ -95,6 +57,44 @@ public class OpenPgpV4Fingerprint extends OpenPgpFingerprint { 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) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java new file mode 100644 index 00000000..fafbc34c --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +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; + +import javax.annotation.Nonnull; +import java.nio.Buffer; +import java.nio.ByteBuffer; + +/** + * This class represents a hex encoded, upper case OpenPGP v5 fingerprint. + */ +public class OpenPgpV5Fingerprint extends OpenPgpFingerprint { + + /** + * Create an {@link OpenPgpV5Fingerprint}. + * + * @param fingerprint uppercase hexadecimal fingerprint of length 64 + */ + public OpenPgpV5Fingerprint(@Nonnull String fingerprint) { + super(fingerprint); + } + + public OpenPgpV5Fingerprint(@Nonnull byte[] bytes) { + super(Hex.encode(bytes)); + } + + public OpenPgpV5Fingerprint(@Nonnull PGPPublicKey key) { + super(key); + } + + public OpenPgpV5Fingerprint(@Nonnull PGPSecretKey key) { + this(key.getPublicKey()); + } + + public OpenPgpV5Fingerprint(@Nonnull PGPPublicKeyRing ring) { + super(ring); + } + + public OpenPgpV5Fingerprint(@Nonnull PGPSecretKeyRing ring) { + super(ring); + } + + public OpenPgpV5Fingerprint(@Nonnull PGPKeyRing ring) { + super(ring); + } + + @Override + public int getVersion() { + return 5; + } + + @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()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java new file mode 100644 index 00000000..f3c10cf7 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class OpenPgpV5FingerprintTest { + + @Test + public void testFingerprintFormatting() { + String pretty = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + String fp = pretty.replace(" ", ""); + + OpenPgpV5Fingerprint fingerprint = new OpenPgpV5Fingerprint(fp); + assertEquals(fp, fingerprint.toString()); + assertEquals(pretty, fingerprint.prettyPrint()); + + long id = fingerprint.getKeyId(); + assertEquals("76543210abcdefab", Long.toHexString(id)); + } +}