diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java index 0525eb5b..43cb8140 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java @@ -136,26 +136,6 @@ public class KeyRingReader { isSilent); } - private static void validateStreamsNotBothNull(InputStream publicIn, InputStream secretIn) { - if (publicIn == null && secretIn == null) { - throw new NullPointerException("publicIn and secretIn cannot be BOTH null."); - } - } - - private static PGPPublicKeyRing maybeReadPublicKeys(InputStream publicIn) throws IOException { - if (publicIn != null) { - return readPublicKeyRing(publicIn); - } - return null; - } - - private static PGPSecretKeyRing maybeReadSecretKeys(InputStream secretIn) throws IOException, PGPException { - if (secretIn != null) { - return readSecretKeyRing(secretIn); - } - return null; - } - /** * Hacky workaround for #96. * For {@link PGPPublicKeyRingCollection#PGPPublicKeyRingCollection(InputStream, KeyFingerPrintCalculator)} 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 555a3369..bb6bad9e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -28,7 +28,9 @@ import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.util.io.Streams; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.key.OpenPgpV4Fingerprint; @@ -54,6 +56,30 @@ public class ArmorUtils { return toAsciiArmoredString(publicKeys.getEncoded(), header); } + public static String toAsciiArmoredString(PGPSecretKeyRingCollection secretKeyRings) throws IOException { + StringBuilder sb = new StringBuilder(); + for (Iterator iterator = secretKeyRings.iterator(); iterator.hasNext(); ) { + PGPSecretKeyRing secretKeyRing = iterator.next(); + sb.append(toAsciiArmoredString(secretKeyRing)); + if (iterator.hasNext()) { + sb.append('\n'); + } + } + return sb.toString(); + } + + public static String toAsciiArmoredString(PGPPublicKeyRingCollection publicKeyRings) throws IOException { + StringBuilder sb = new StringBuilder(); + for (Iterator iterator = publicKeyRings.iterator(); iterator.hasNext(); ) { + PGPPublicKeyRing publicKeyRing = iterator.next(); + sb.append(toAsciiArmoredString(publicKeyRing)); + if (iterator.hasNext()) { + sb.append('\n'); + } + } + return sb.toString(); + } + private static MultiMap keyToHeader(PGPKeyRing keyRing) { MultiMap header = new MultiMap<>(); OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(keyRing); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingCollectionReaderTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingCollectionReaderTest.java new file mode 100644 index 00000000..2027a243 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingCollectionReaderTest.java @@ -0,0 +1,169 @@ +/* + * Copyright 2021 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pgpainless.key.parsing; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Iterator; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.util.ArmorUtils; + +public class KeyRingCollectionReaderTest { + + @Test + public void writeAndParseKeyRingCollections() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + // secret keys + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice ", null); + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("Bob ", null); + + PGPSecretKeyRingCollection collection = KeyRingUtils.keyRingsToKeyRingCollection(alice, bob); + String ascii = ArmorUtils.toAsciiArmoredString(collection); + + PGPSecretKeyRingCollection parsed = PGPainless.readKeyRing().secretKeyRingCollection(ascii); + assertEquals(collection.size(), parsed.size()); + + // public keys + PGPPublicKeyRing pAlice = KeyRingUtils.publicKeyRingFrom(alice); + PGPPublicKeyRing pBob = KeyRingUtils.publicKeyRingFrom(bob); + + PGPPublicKeyRingCollection pCollection = KeyRingUtils.keyRingsToKeyRingCollection(pAlice, pBob); + ascii = ArmorUtils.toAsciiArmoredString(pCollection); + + PGPPublicKeyRingCollection pParsed = PGPainless.readKeyRing().publicKeyRingCollection(ascii); + assertEquals(pCollection.size(), pParsed.size()); + } + + @Test + public void parseSeparatedSecretKeyRingCollection() throws PGPException, IOException { + String ascii = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 58F2 0119 232F BBC0 B624 CCA7 7BED B6B3 2279 0657\n" + + "Comment: Alice \n" + + "\n" + + "lFgEYLIldRYJKwYBBAHaRw8BAQdAv06tp4xghoxP/oDnIXuB//vH0RajTK7urjNn\n" + + "8YlYnucAAPsFAWLAW0c70rSktFw4CbtelRvtkcsGQkJVXXekRPcrGQ5jtBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iHgEExYKACAFAmCyJXUCGwEFFgIDAQAE\n" + + "CwkIBwUVCgkICwIeAQIZAQAKCRB77bazInkGV9XIAP9M1yDWCPta2hMoNlKj74Yo\n" + + "kQXSI0VQT3FFq4ZIre5n9QEAxJTiMs+vhnmWChXz2RXvoqP/NdSYWZ6TLnqUy1Tz\n" + + "JQ2cXQRgsiV1EgorBgEEAZdVAQUBAQdAMUoA28ic8ZfbCzw3z60T3kmQNWQqdTQs\n" + + "HuxEQPj2B24DAQgHAAD/SWLvXh81Ho+6dysWNd9/qmtx0vcF1NeBsRu/Z+noe7gR\n" + + "c4h1BBgWCgAdBQJgsiV1AhsMBRYCAwEABAsJCAcFFQoJCAsCHgEACgkQe+22syJ5\n" + + "BleuPwEAvzGxpoCl4cRWk6t+UZdCALMdnM050sf0jruryQhg8lkBANa3i54K5Eze\n" + + "2ah+1f5O8JLudv5t9NS1kERY2JpqVlAPnFgEYLIldRYJKwYBBAHaRw8BAQdAO0VF\n" + + "ebLPMAYaxGl99jyLkQEJ4wNgdI1rBn3SDYnUq3kAAP4ugbF5XlRNHzxnSubS7Byf\n" + + "bF9gnmFt8eCQWdTM0FwUvREviNUEGBYKAH0FAmCyJXUCGwIFFgIDAQAECwkIBwUV\n" + + "CgkICwIeAV8gBBkWCgAGBQJgsiV1AAoJEOTg022wUXnBdqoA/1LjvNS65BieQ1uc\n" + + "l0kleh+K3rm4nFTs9dE39mbAI0k1AP9uHb4ucGunvqkq9x2nuFzCZHLoaBrzgi9S\n" + + "nGvLiHzLBgAKCRB77bazInkGV5S5AP433Ln47AHNr4u8Jo5aU5ML5f5KcxaOhQES\n" + + "SCBQ71BYWQEAlBFEhROHvJB2NCH695/zp5z5O6tmA0rLSQxZUTvyuQg=\n" + + "=Iwkd\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 88DD 9483 8B3D AB5B A07B 8F77 C852 EE0C 9502 F445\n" + + "Comment: Bob \n" + + "\n" + + "lFgEYLIldRYJKwYBBAHaRw8BAQdAY9AtZCfF3C8fLJ81o9qVlK4h6vgT///jGX6A\n" + + "qg/LsF4AAQDM5uiSRDYQBNqA/DydySUNLfjMvI4Aa7ONYwLqGoOvQA+mtBhCb2Ig\n" + + "PGJvYkBwZ3BhaW5sZXNzLm9yZz6IeAQTFgoAIAUCYLIldQIbAQUWAgMBAAQLCQgH\n" + + "BRUKCQgLAh4BAhkBAAoJEMhS7gyVAvRFdPgBAN36fO2Oo7iXukCgzOVRxb2sE1Ay\n" + + "+pWE+Vpt2Y4NiUrVAQCKmD0hl3SIolJf+sFpInToqT7s1P34o4hYPozEDj1IBZxd\n" + + "BGCyJXUSCisGAQQBl1UBBQEBB0C48wzNDfxyS/vjXNDWj06C4TLiu9JizHP1SQzN\n" + + "vs2YNQMBCAcAAP95XEFiQHLBbmpwvZiSRCt7MjXe4ODk+LPY787YyGiImBNUiHUE\n" + + "GBYKAB0FAmCyJXUCGwwFFgIDAQAECwkIBwUVCgkICwIeAQAKCRDIUu4MlQL0RVX1\n" + + "AP0Y1E2XEZZSBjU6a3LDY7so5h/WKyj2wFhPNlYJMPyEwAD/YwUd7K3Iu2jnSRyQ\n" + + "YkMPpBlUiCzY1WsPrYIpsrlhsAicWARgsiV1FgkrBgEEAdpHDwEBB0D2w+nDeSk1\n" + + "X8sGbIDc0eajB0nYaGoZ61LGjmJRXyxn/QABANmdFE//RkuC9vq150kbIXzjrm54\n" + + "TJ/l3HLv2Vb9JV5oEhSI1QQYFgoAfQUCYLIldQIbAgUWAgMBAAQLCQgHBRUKCQgL\n" + + "Ah4BXyAEGRYKAAYFAmCyJXUACgkQ9mL4hDfRd/aN9AEAnI2ssrPZwREpOcZsrYIe\n" + + "xSRFKc8n8RMDizHgnSyj3ZgBAPVceQEU78wnatz/x/Jbr2hE9Pj8IJK8fT96aXti\n" + + "CEEOAAoJEMhS7gyVAvRFw+0A/34n6qI1mJuXUNWdJd2yiGCKXLvVkwvpn2wQ5kaX\n" + + "9/m2AQCJC+MXorN3ro7aGtlz/81rtHREZftt2YH+pAy2OWq/BQ==\n" + + "=JB3F\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + PGPSecretKeyRingCollection collection = PGPainless.readKeyRing().secretKeyRingCollection(ascii); + assertEquals(2, collection.size()); + Iterator iterator = collection.getKeyRings(); + assertEquals(new OpenPgpV4Fingerprint("58F2 0119 232F BBC0 B624 CCA7 7BED B6B3 2279 0657"), + new OpenPgpV4Fingerprint(iterator.next())); + assertEquals(new OpenPgpV4Fingerprint("88DD 9483 8B3D AB5B A07B 8F77 C852 EE0C 9502 F445"), + new OpenPgpV4Fingerprint(iterator.next())); + } + + @Test + public void parseConcatenatedSecretKeyRingCollection() throws PGPException, IOException { + // same key ring collection as above, but concatenated in a single armor block + String ascii = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: BCPG v1.68\n" + + "\n" + + "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\n" + + "=TTn+\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + PGPSecretKeyRingCollection collection = PGPainless.readKeyRing().secretKeyRingCollection(ascii); + assertEquals(2, collection.size()); + Iterator iterator = collection.getKeyRings(); + assertEquals(new OpenPgpV4Fingerprint("58F2 0119 232F BBC0 B624 CCA7 7BED B6B3 2279 0657"), + new OpenPgpV4Fingerprint(iterator.next())); + assertEquals(new OpenPgpV4Fingerprint("88DD 9483 8B3D AB5B A07B 8F77 C852 EE0C 9502 F445"), + new OpenPgpV4Fingerprint(iterator.next())); + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java index 4eaac5aa..786e8a95 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java @@ -25,6 +25,7 @@ import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.pgpainless.PGPainless; @@ -47,10 +48,13 @@ public class SopKeyUtil { List publicKeyRings = new ArrayList<>(); for (File file : files) { try (FileInputStream in = new FileInputStream(file)) { - publicKeyRings.add(PGPainless.readKeyRing().publicKeyRing(in)); - } catch (IOException e) { + PGPPublicKeyRingCollection collection = PGPainless.readKeyRing().publicKeyRingCollection(in); + for (PGPPublicKeyRing keyRing : collection) { + publicKeyRings.add(keyRing); + } + } catch (IOException | PGPException e) { err_ln("Could not read certificate from file " + file.getName() + ": " + e.getMessage()); - throw e; + throw new IOException(e); } } return publicKeyRings; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java index 1e0ab95d..0456fb47 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java @@ -17,6 +17,7 @@ package org.pgpainless.sop.commands; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; @@ -88,7 +89,7 @@ public class Verify implements Runnable { Date notBeforeDate = parseNotBefore(); Date notAfterDate = parseNotAfter(); - Map publicKeys = readCertificatesFromFiles(); + Map publicKeys = readCertificatesFromFiles(); if (publicKeys.isEmpty()) { err_ln("No certificates supplied."); System.exit(19); @@ -100,7 +101,7 @@ public class Verify implements Runnable { .onInputStream(System.in) .doNotDecrypt() .verifyDetachedSignature(sigIn) - .verifyWith(new HashSet<>(publicKeys.values())) + .verifyWith(new HashSet<>(publicKeys.keySet())) .ignoreMissingPublicKeys() .build(); @@ -138,30 +139,33 @@ public class Verify implements Runnable { printValidSignatures(signaturesInTimeRange, publicKeys); } - private void printValidSignatures(Map validSignatures, Map publicKeys) { + private void printValidSignatures(Map validSignatures, Map publicKeys) { for (OpenPgpV4Fingerprint sigKeyFp : validSignatures.keySet()) { PGPSignature signature = validSignatures.get(sigKeyFp); - for (File file : publicKeys.keySet()) { + for (PGPPublicKeyRing ring : publicKeys.keySet()) { // Search signing key ring - PGPPublicKeyRing publicKeyRing = publicKeys.get(file); - if (publicKeyRing.getPublicKey(sigKeyFp.getKeyId()) == null) { + File file = publicKeys.get(ring); + if (ring.getPublicKey(sigKeyFp.getKeyId()) == null) { continue; } String utcSigDate = df.format(signature.getCreationTime()); - OpenPgpV4Fingerprint primaryKeyFp = new OpenPgpV4Fingerprint(publicKeyRing); + OpenPgpV4Fingerprint primaryKeyFp = new OpenPgpV4Fingerprint(ring); print_ln(utcSigDate + " " + sigKeyFp.toString() + " " + primaryKeyFp.toString() + " signed by " + file.getName()); } } } - private Map readCertificatesFromFiles() { - Map publicKeys = new HashMap<>(); + private Map readCertificatesFromFiles() { + Map publicKeys = new HashMap<>(); for (File cert : certificates) { try (FileInputStream in = new FileInputStream(cert)) { - publicKeys.put(cert, PGPainless.readKeyRing().publicKeyRing(in)); - } catch (IOException e) { + PGPPublicKeyRingCollection collection = PGPainless.readKeyRing().publicKeyRingCollection(in); + for (PGPPublicKeyRing ring : collection) { + publicKeys.put(ring, cert); + } + } catch (IOException | PGPException e) { err_ln("Cannot read certificate from file " + cert.getAbsolutePath() + ":"); err_ln(e.getMessage()); }