diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index 9d73635e..122dd961 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -24,6 +24,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.util.io.Streams; @@ -106,14 +107,38 @@ public final class ArmorUtils { return sb.toString(); } + private static Tuple getPrimaryUserIdAndUserIdCount(PGPPublicKey publicKey) { + Iterator userIds = publicKey.getUserIDs(); + int countIdentities = 0; + String primary = null; + while (userIds.hasNext()) { + countIdentities++; + String userId = userIds.next(); + if (primary == null) { + Iterator signatures = publicKey.getSignaturesForID(userId); + while (signatures.hasNext()) { + PGPSignature signature = signatures.next(); + if (signature.getHashedSubPackets().isPrimaryUserID()) { + primary = userId; + break; + } + } + } + } + return new Tuple<>(primary, countIdentities); + } + private static MultiMap keyToHeader(PGPPublicKey publicKey) { MultiMap header = new MultiMap<>(); OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); - Iterator userIds = publicKey.getUserIDs(); header.put(HEADER_COMMENT, fingerprint.prettyPrint()); - if (userIds.hasNext()) { - header.put(HEADER_COMMENT, userIds.next()); + Tuple idCount = getPrimaryUserIdAndUserIdCount(publicKey); + if (idCount.getA() != null) { + header.put(HEADER_COMMENT, idCount.getA() + " (Primary)"); + } + if (idCount.getB() != 1) { + header.put(HEADER_COMMENT, String.format("Public key contains %d identities", idCount.getB())); } return header; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java index f421277b..d9ac2ca5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java @@ -21,16 +21,24 @@ import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; +import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.TestKeys; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.ecc.EllipticCurve; +import org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA; public class ArmorUtilsTest { @@ -144,6 +152,53 @@ public class ArmorUtilsTest { "-----END PGP MESSAGE-----\n", out.toString()); } + @Test + public void testMultipleIdentitiesInHeader() throws Exception { + PGPSecretKeyRing secretKeyRing = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(ECDSA.fromCurve(EllipticCurve._P256), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) + .addUserId("Juliet ") + .addUserId("xmpp:juliet@capulet.lit") + .setPassphrase(Passphrase.fromPassword("test")) + .build(); + PGPPublicKey publicKey = secretKeyRing.getPublicKey(); + PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); + String armored = PGPainless.asciiArmor(publicKeyRing); + Assertions.assertTrue(armored.contains("Comment: Juliet (Primary)")); + Assertions.assertTrue(armored.contains("Comment: Public key contains 2 identities")); + } + + @Test + public void testSingleIdentityInHeader() throws Exception { + PGPSecretKeyRing secretKeyRing = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(ECDSA.fromCurve(EllipticCurve._P256), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) + .addUserId("Juliet ") + .setPassphrase(Passphrase.fromPassword("test")) + .build(); + PGPPublicKey publicKey = secretKeyRing.getPublicKey(); + PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); + String armored = PGPainless.asciiArmor(publicKeyRing); + Assertions.assertTrue(armored.contains("Comment: Juliet (Primary)")); + Assertions.assertFalse(armored.contains("Comment: Public key contains 1 identities")); + } + + @Test + public void testWithoutIdentityInHeader() throws Exception { + PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing("-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xsBNBGIgzE0BCACwxaYg6bpmp0POq1T6yalGE9XaL2IG9d9khDBweZ63s3Pu1pHB\n" + + "JtmjgN7Tx3ts6hLzQm3YKYA6zu1MXQ8k2vqtdtGUpZPp18Pbars7yUDqh8QIdFjO\n" + + "GeE+c8So0MQgTgoBuyZiSmslwp1WO78ozf/0rCayFdy73dPUntuLE6c2ZKO8nw/g\n" + + "uyk2ozsqLN/TBpgbuJUyMedJtXV10DdT9QxH/66LmdjFKXTkc74qI8YAm/pmJeOh\n" + + "36qZ5ehAgz9MthPQINnZKpnqidqkGFvjwVFlCMlVSmNCNJmpgGDH3gvkklZHzGsf\n" + + "dfzQswd/BQjPsFH9cK+QFYMG6q2zrvM0X9mdABEBAAE=\n" + + "=njg8\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"); + PGPPublicKey publicKey = publicKeys.getPublicKey(); + PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); + String armored = PGPainless.asciiArmor(publicKeyRing); + Assertions.assertTrue(armored.contains("Comment: Public key contains 0 identities")); + } + @TestTemplate @ExtendWith(TestAllImplementations.class) public void decodeExampleTest() throws IOException, PGPException {