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 0525f4c9..1ac900a0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java @@ -36,6 +36,9 @@ public abstract class OpenPgpFingerprint implements CharSequence, Comparable +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +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; + +/** + * This class represents a hex encoded, upper case OpenPGP v6 fingerprint. + */ +public class OpenPgpV6Fingerprint extends _64DigitFingerprint { + + /** + * Create an {@link OpenPgpV6Fingerprint}. + * + * @param fingerprint uppercase hexadecimal fingerprint of length 64 + */ + public OpenPgpV6Fingerprint(@Nonnull String fingerprint) { + super(fingerprint); + } + + public OpenPgpV6Fingerprint(@Nonnull byte[] bytes) { + super(bytes); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPPublicKey key) { + super(key); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPSecretKey key) { + this(key.getPublicKey()); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPPublicKeyRing ring) { + super(ring); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPSecretKeyRing ring) { + super(ring); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPKeyRing ring) { + super(ring); + } + + @Override + public int getVersion() { + return 6; + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java new file mode 100644 index 00000000..11f18058 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// 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()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java index a250bef4..57c98928 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java @@ -5,8 +5,6 @@ package org.pgpainless.key; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -29,77 +27,12 @@ public class OpenPgpV5FingerprintTest { OpenPgpV5Fingerprint fingerprint = new OpenPgpV5Fingerprint(fp); assertEquals(fp, fingerprint.toString()); assertEquals(pretty, fingerprint.prettyPrint()); + assertEquals(5, fingerprint.getVersion()); long id = fingerprint.getKeyId(); assertEquals("76543210abcdefab", Long.toHexString(id)); } - @Test - public void testParse() { - String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; - OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); - - assertTrue(parsed instanceof OpenPgpV5Fingerprint); - OpenPgpV5Fingerprint v5fp = (OpenPgpV5Fingerprint) parsed; - assertEquals(prettyPrint, v5fp.prettyPrint()); - assertEquals(5, v5fp.getVersion()); - } - - @Test - public void testParseFromBinary() { - String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; - byte[] binary = Hex.decode(hex); - - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); - assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); - assertEquals(hex, fingerprint.toString()); - - OpenPgpV5Fingerprint constructed = new OpenPgpV5Fingerprint(binary); - assertEquals(fingerprint, constructed); - } - - @Test - public void testParseFromBinary_leadingZeros() { - String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; - byte[] binary = Hex.decode(hex); - - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); - assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); - assertEquals(hex, fingerprint.toString()); - } - - @Test - public void testParseFromBinary_trailingZeros() { - String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA100000000000000000"; - byte[] binary = Hex.decode(hex); - - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); - assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); - assertEquals(hex, fingerprint.toString()); - } - - @Test - public void testParseFromBinary_wrongLength() { - String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA012345"; // missing 2 digits - byte[] binary = Hex.decode(hex); - - assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.parseFromBinary(binary)); - } - - @Test - public void equalsTest() { - String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; - OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); - - assertNotEquals(parsed, null); - assertNotEquals(parsed, new Object()); - assertEquals(parsed, parsed.toString()); - - OpenPgpFingerprint parsed2 = new OpenPgpV5Fingerprint(prettyPrint); - assertEquals(parsed.hashCode(), parsed2.hashCode()); - assertEquals(0, parsed.compareTo(parsed2)); - } - @Test public void constructFromMockedPublicKey() { String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java new file mode 100644 index 00000000..7a7b0e45 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java @@ -0,0 +1,154 @@ +// 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 org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OpenPgpV6FingerprintTest { + + @Test + public void testFingerprintFormatting() { + String pretty = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + String fp = pretty.replace(" ", ""); + + OpenPgpV6Fingerprint fingerprint = new OpenPgpV6Fingerprint(fp); + assertEquals(fp, fingerprint.toString()); + assertEquals(pretty, fingerprint.prettyPrint()); + assertEquals(6, fingerprint.getVersion()); + + long id = fingerprint.getKeyId(); + assertEquals("76543210abcdefab", Long.toHexString(id)); + } + + @Test + public void testParseFromBinary_leadingZeros() { + String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = new OpenPgpV6Fingerprint(binary); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_trailingZeros() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA100000000000000000"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = new OpenPgpV6Fingerprint(binary); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_wrongLength() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA012345"; // missing 2 digits + byte[] binary = Hex.decode(hex); + + assertThrows(IllegalArgumentException.class, () -> new OpenPgpV6Fingerprint(binary)); + } + + @Test + public void equalsTest() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = new OpenPgpV6Fingerprint(prettyPrint); + + assertNotEquals(parsed, null); + assertNotEquals(parsed, new Object()); + assertEquals(parsed, parsed.toString()); + + OpenPgpFingerprint parsed2 = new OpenPgpV6Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), parsed2.hashCode()); + assertEquals(0, parsed.compareTo(parsed2)); + } + + @Test + public void constructFromMockedPublicKey() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); + assertTrue(fingerprint instanceof OpenPgpV6Fingerprint); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedSecretKey() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPSecretKey secretKey = mock(PGPSecretKey.class); + when(secretKey.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = new OpenPgpV6Fingerprint(secretKey); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedPublicKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPPublicKeyRing publicKeys = mock(PGPPublicKeyRing.class); + when(publicKeys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKeys); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV6Fingerprint(publicKeys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedSecretKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPSecretKeyRing secretKeys = mock(PGPSecretKeyRing.class); + when(secretKeys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(secretKeys); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV6Fingerprint(secretKeys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPKeyRing keys = mock(PGPKeyRing.class); + when(keys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(keys); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV6Fingerprint(keys); + assertEquals(hex, fingerprint.toString()); + } + + private PGPPublicKey getMockedPublicKey(String hex) { + byte[] binary = Hex.decode(hex); + + PGPPublicKey mocked = mock(PGPPublicKey.class); + when(mocked.getVersion()).thenReturn(6); + when(mocked.getFingerprint()).thenReturn(binary); + return mocked; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java new file mode 100644 index 00000000..21ff31c3 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java @@ -0,0 +1,85 @@ +package org.pgpainless.key; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class _64DigitFingerprintTest { + + @Test + public void testParse() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); + + assertTrue(parsed instanceof _64DigitFingerprint); + assertEquals(prettyPrint, parsed.prettyPrint()); + assertEquals(-1, parsed.getVersion()); + } + + @Test + public void testParseFromBinary() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof _64DigitFingerprint); + assertEquals(hex, fingerprint.toString()); + + OpenPgpV5Fingerprint v5 = new OpenPgpV5Fingerprint(binary); + assertEquals(fingerprint, v5); + + OpenPgpV6Fingerprint v6 = new OpenPgpV6Fingerprint(binary); + assertEquals(fingerprint, v6); + } + + @Test + public void testParseFromBinary_leadingZeros() { + String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof _64DigitFingerprint); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_trailingZeros() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA100000000000000000"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof _64DigitFingerprint); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_wrongLength() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA012345"; // missing 2 digits + byte[] binary = Hex.decode(hex); + + assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.parseFromBinary(binary)); + } + + @Test + public void equalsTest() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); + + assertNotEquals(parsed, null); + assertNotEquals(parsed, new Object()); + assertEquals(parsed, parsed.toString()); + + OpenPgpFingerprint v5 = new OpenPgpV5Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), v5.hashCode()); + assertEquals(0, parsed.compareTo(v5)); + + OpenPgpFingerprint v6 = new OpenPgpV6Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), v6.hashCode()); + assertEquals(0, parsed.compareTo(v6)); + } + +}