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 6f3a1c2c..66884cb2 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 @@ -27,6 +27,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.pgpainless.implementation.ImplementationFactory; public class KeyRingReader { @@ -96,7 +97,7 @@ public class KeyRingReader { public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream) throws IOException, PGPException { return new PGPPublicKeyRingCollection( - PGPUtil.getDecoderStream(inputStream), + getDecoderStream(inputStream), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); } @@ -109,7 +110,7 @@ public class KeyRingReader { public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream) throws IOException, PGPException { return new PGPSecretKeyRingCollection( - PGPUtil.getDecoderStream(inputStream), + getDecoderStream(inputStream), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); } @@ -132,4 +133,23 @@ public class KeyRingReader { } return null; } + + /** + * Hacky workaround for #96. + * For {@link PGPPublicKeyRingCollection#PGPPublicKeyRingCollection(InputStream, KeyFingerPrintCalculator)} + * or {@link PGPSecretKeyRingCollection#PGPSecretKeyRingCollection(InputStream, KeyFingerPrintCalculator)} + * to read all PGPKeyRings properly, we apparently have to make sure that the {@link InputStream} that is given + * as constructor argument is a {@link PGPUtil.BufferedInputStreamExt}. + * Since {@link PGPUtil#getDecoderStream(InputStream)} will return an {@link org.bouncycastle.bcpg.ArmoredInputStream} + * if the underlying input stream contains armored data, we have to nest two method calls to make sure that the + * end-result is a {@link PGPUtil.BufferedInputStreamExt}. + * + * This is a hacky solution. + * + * @param inputStream input stream + * @return BufferedInputStreamExt + */ + private static InputStream getDecoderStream(InputStream inputStream) throws IOException { + return PGPUtil.getDecoderStream(PGPUtil.getDecoderStream(inputStream)); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java new file mode 100644 index 00000000..6efb9d4f --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java @@ -0,0 +1,101 @@ +/* + * 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.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collection; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPUtil; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.util.KeyRingUtils; + +class KeyRingReaderTest { + + @Test + public void assertThatPGPUtilsDetectAsciiArmoredData() throws IOException, PGPException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("pub_keys_10_pieces.asc"); + + InputStream possiblyArmored = PGPUtil.getDecoderStream(PGPUtil.getDecoderStream(inputStream)); + + PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection( + possiblyArmored, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); + assertEquals(10, collection.size()); + } + + @Test + void publicKeyRingCollectionFromStream() throws IOException, PGPException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("pub_keys_10_pieces.asc"); + PGPPublicKeyRingCollection rings = PGPainless.readKeyRing().publicKeyRingCollection(inputStream); + assertEquals(10, rings.size()); + } + + @Test + void publicKeyRingCollectionFromNotArmoredStream() throws IOException, PGPException, + InvalidAlgorithmParameterException, NoSuchAlgorithmException { + Collection collection = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("user_" + i + "@encrypted.key"); + collection.add(KeyRingUtils.publicKeyRingFrom(secretKeys)); + } + + PGPPublicKeyRingCollection originalRings = new PGPPublicKeyRingCollection(collection); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + originalRings.encode(out); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(out.toByteArray()); + PGPPublicKeyRingCollection parsedRings = PGPainless.readKeyRing().publicKeyRingCollection(inputStream); + assertEquals(10, parsedRings.size()); + } + + @Test + void publicKeyRingCollectionFromString() throws IOException, PGPException, URISyntaxException { + URL resource = getClass().getClassLoader().getResource("pub_keys_10_pieces.asc"); + String armoredString = new String(Files.readAllBytes(new File(resource.toURI()).toPath())); + InputStream inputStream = new ByteArrayInputStream(armoredString.getBytes(StandardCharsets.UTF_8)); + PGPPublicKeyRingCollection rings = PGPainless.readKeyRing().publicKeyRingCollection(inputStream); + assertEquals(10, rings.size()); + } + + @Test + void publicKeyRingCollectionFromBytes() throws IOException, PGPException, URISyntaxException { + URL resource = getClass().getClassLoader().getResource("pub_keys_10_pieces.asc"); + byte[] bytes = Files.readAllBytes(new File(resource.toURI()).toPath()); + InputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + PGPPublicKeyRingCollection rings = PGPainless.readKeyRing().publicKeyRingCollection(byteArrayInputStream); + assertEquals(10, rings.size()); + } + +} diff --git a/pgpainless-core/src/test/resources/pub_keys_10_pieces.asc b/pgpainless-core/src/test/resources/pub_keys_10_pieces.asc new file mode 100644 index 00000000..01f91368 --- /dev/null +++ b/pgpainless-core/src/test/resources/pub_keys_10_pieces.asc @@ -0,0 +1,139 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: PGPainless + +mDMEYFHdchYJKwYBBAHaRw8BAQdAyfub9GUlTvuIicT6gnLTe5IK7ulwRjh6AmNW +9pGVpb60E2ZyZXNoQGVuY3J5cHRlZC5rZXmIeAQTFgoAIAUCYFHdcgIbAwUWAgMB +AAQLCQgHBRUKCQgLAh4BAhkBAAoJEJSMRVGPhjdgOUABAIU80GMm2v8oWrQyctAi +VhpkRhNXFBoxzV2Ocg47eWukAQCHjjA10tTIzq5umoNXrb1vLXmubzT77fXJwFA2 +qUonD7g4BGBR3XISCisGAQQBl1UBBQEBB0AZuOnF7dxM/+U9kZtHkf4ze9mlm7mF +nzt2ugq/LrgLKAMBCAeIdQQYFgoAHQUCYFHdcgIbDAUWAgMBAAQLCQgHBRUKCQgL +Ah4BAAoJEJSMRVGPhjdghv8BAMYlDl20Kn3K1KWFBSU8bw7OZsgz5ppzEywU/xpr +v2yHAP470or42l5oTh/xAnWQEReD0d5pnh5fIKkeWBQTL7Y9Bg== +=L01J +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: PGPainless + +mDMEYFHdchYJKwYBBAHaRw8BAQdA3zRVckfne2fxGDOv5c2Tm7C4Azxbc+CZKP1R ++XjpLKG0E2ZyZXNoQGVuY3J5cHRlZC5rZXmIeAQTFgoAIAUCYFHdcgIbAwUWAgMB +AAQLCQgHBRUKCQgLAh4BAhkBAAoJEBOTT2Wz+JaqlwwA/3k6FiMD9hsPATpPgsmB +dG6oz/pklaMlUvEzCh7NFRQqAP40nX6T3L4UwYbrvm4FXTle3DaL98DfFjH8ox1N +e/ziC7g4BGBR3XISCisGAQQBl1UBBQEBB0BRwE/EJ/ocQ3nqm4jv8tB0nYYliJ72 +tvDVQhsD4mQLIAMBCAeIdQQYFgoAHQUCYFHdcgIbDAUWAgMBAAQLCQgHBRUKCQgL +Ah4BAAoJEBOTT2Wz+JaqztIBAJvwyo0pw+q+48MYd0yIKRavkJ2Y71msPwSQ7Tnj +gK7ZAQDW9L8pc9m1WISpI4HqVUGBbiaEm/U9vdNZ22fiehK+Dw== +=F7rW +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: PGPainless + +mDMEYFHdchYJKwYBBAHaRw8BAQdA4N4vZfhHcDvneIkPyTnBt4Ms3MSJAUwnxKKa +hFs7tKS0E2ZyZXNoQGVuY3J5cHRlZC5rZXmIeAQTFgoAIAUCYFHdcgIbAwUWAgMB +AAQLCQgHBRUKCQgLAh4BAhkBAAoJEONPfCWrUaykZC8BAJ0QAfFQAT4rSx3SWc8I +FTHIflpj6G9OiV5EZcbDlUArAP0XtetsnNdba5wpeITZpL+/L3lgeb9pphr/rA/8 +/LLYDbg4BGBR3XISCisGAQQBl1UBBQEBB0DDwrx6U+u5yTlagwJHlSenorBjF6B2 +dAhNwblxhGA8fQMBCAeIdQQYFgoAHQUCYFHdcgIbDAUWAgMBAAQLCQgHBRUKCQgL +Ah4BAAoJEONPfCWrUaykxQgBANUWnD5V2HhWTmi0ArVNKivW4Mvtj8GnH1m37qPp +3D8VAP9SLOQrdGj1kFBtQSefkUCgMHdT1cddL919y8nRIevdAw== +=9/eE +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: PGPainless + +mDMEYFHdchYJKwYBBAHaRw8BAQdAflONZMOFFXT8HEwrjeRowixJ/CQwu1jF8WIf +I4MYreq0E2ZyZXNoQGVuY3J5cHRlZC5rZXmIeAQTFgoAIAUCYFHdcgIbAwUWAgMB +AAQLCQgHBRUKCQgLAh4BAhkBAAoJEGUsIC1Do6/1JisBAKTbbh6CaxhR9C0GqGMq +Bd7y36wkd+qw7MyR63dUj/6MAQCkt+4h/YbiWnMFL1pLY+u5rERyyT4zDlZTDDV0 +vHsUB7g4BGBR3XISCisGAQQBl1UBBQEBB0AEFOE5+gHfw50UxFxY2MGT6e/M2oUB +MBaOsIsVOPuZZwMBCAeIdQQYFgoAHQUCYFHdcgIbDAUWAgMBAAQLCQgHBRUKCQgL +Ah4BAAoJEGUsIC1Do6/1EusBAL/IRKILeXCwAnXGfpnO3smCFSsGtCYJ1rhVaYhj +zeVtAP47EXBKYvhbHgRy+Qx2BMFXX+VGFA2hWHZbo9OhbPpTAg== +=cCbp +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: PGPainless + +mDMEYFHdchYJKwYBBAHaRw8BAQdAYXkCPGptUC4P0LeQCC0kzi5PnzcZAFfX8S5b +t8hRITy0E2ZyZXNoQGVuY3J5cHRlZC5rZXmIeAQTFgoAIAUCYFHdcgIbAwUWAgMB +AAQLCQgHBRUKCQgLAh4BAhkBAAoJEIJgTO3chyrRB2UA+wfyFgnPEd8xkAwlPPJ7 +Bo0Ou6qFWzgigG1mGw2yHfogAQD5SW73I2bByhyP8LfyYRSQGjM6msxkECVDEVUw +/hxaCbg4BGBR3XISCisGAQQBl1UBBQEBB0DUh3Sai70XUwzPQpe2mqppwn9ApUCk +hStE1Giy4bzNdgMBCAeIdQQYFgoAHQUCYFHdcgIbDAUWAgMBAAQLCQgHBRUKCQgL +Ah4BAAoJEIJgTO3chyrRgusA/3TbfgIiRL3Ype2LlfEZ2evyo1T8hQlH5yOmbKGW +UxOIAQDWufEmTWpeV+bWfSkcb1C7dU8wSvM78GvAMMESrxozCw== +=stfa +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: PGPainless + +mDMEYFHdchYJKwYBBAHaRw8BAQdAPybWtX75zj1GIjPEITKpjyG7MccvPLoU9HrU +d3QwWmO0E2ZyZXNoQGVuY3J5cHRlZC5rZXmIeAQTFgoAIAUCYFHdcgIbAwUWAgMB +AAQLCQgHBRUKCQgLAh4BAhkBAAoJEI1HfhWTysXbKqAA/igvr9eCnSGKzYD6KnrZ +wd6MKM4OD5NsiX6DkCew/AaPAQDK9lyRdibbR52CNVeueVYkCbMaPohChf9nvx2M +4XLcBbg4BGBR3XISCisGAQQBl1UBBQEBB0AfsNRAXC/naStyC9666gAOtgEOqQ3e +XhxgfITehCabBQMBCAeIdQQYFgoAHQUCYFHdcgIbDAUWAgMBAAQLCQgHBRUKCQgL +Ah4BAAoJEI1HfhWTysXb2AIBALmv9be7RWcRnHvvVw3sCqzH5P3dQlOiYkzp++wj +HFBQAQC1XRes+v9nawkkQPLL6W6TTdJsrqdRTghLO9oWad7DAQ== +=ILBW +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: PGPainless + +mDMEYFHdchYJKwYBBAHaRw8BAQdAtW6tSlO27WD5JWBmfzfmy/VLcB7yfPdvqxux +byKVk6O0E2ZyZXNoQGVuY3J5cHRlZC5rZXmIeAQTFgoAIAUCYFHdcgIbAwUWAgMB +AAQLCQgHBRUKCQgLAh4BAhkBAAoJEP9Vh6wldIQkKwABAKC8xwjkT4qeOWV2bKu/ +3C7a1T4hlhQtRqSiFcdK9pDrAQCAdN1+XDkoKz6b717PcRc+6vhUJWGO4GaoCucS +cIuiD7g4BGBR3XISCisGAQQBl1UBBQEBB0C5b5/Zh4wZUOqihUebwjS8djSDvn2h +AuzD3w0FKXgiGQMBCAeIdQQYFgoAHQUCYFHdcgIbDAUWAgMBAAQLCQgHBRUKCQgL +Ah4BAAoJEP9Vh6wldIQkjxIBAOx8lL/3oYnA5CxueagTmGJSl8CvFiirnfzDF34/ +zCx6AP9viCSUII6a9z2vphe7XWRm1VOZNDG4R5Pzg2x32B7tAQ== +=oiKv +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: PGPainless + +mDMEYFHdchYJKwYBBAHaRw8BAQdAo0Hx0pRqnU4mZwfK3CgwzV53QHhMIfltKOM8 +i6kzIN60E2ZyZXNoQGVuY3J5cHRlZC5rZXmIeAQTFgoAIAUCYFHdcgIbAwUWAgMB +AAQLCQgHBRUKCQgLAh4BAhkBAAoJEMrL/Yggs136P+kA/3doLAoG5jju2MZFZAeW +zeBQFCfb103lvYeWGxU9BlhNAP49Ce/Y8SsokcQwmsR8datld+Pe/QmF4/zQl2+R +D30tB7g4BGBR3XISCisGAQQBl1UBBQEBB0DOngNGofghQ3QrqoO/WUJ958cJMGmE +wInz5Gfiby0BOAMBCAeIdQQYFgoAHQUCYFHdcgIbDAUWAgMBAAQLCQgHBRUKCQgL +Ah4BAAoJEMrL/Yggs1365w0A/26IRKTvZpcIGIypXY5B9v5MySrCJJeMFaBTZ2oP +q0MDAP9vWnMlAbAGkdRqbw6uA+rKuTyTnOR7sIroMWQXOy9pBw== +=5H8m +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: PGPainless + +mDMEYFHdchYJKwYBBAHaRw8BAQdA8RbR/TCSQTRszBfELI+TckBaXusWSzQWmJ+A +m6R2vdO0E2ZyZXNoQGVuY3J5cHRlZC5rZXmIeAQTFgoAIAUCYFHdcgIbAwUWAgMB +AAQLCQgHBRUKCQgLAh4BAhkBAAoJECFoxR5ec+rpzCsBAOPe/5BHmPuS6lRDPFK2 +Ww2lgEaCo0z88mLKp2DTzzwlAQDlnlrUpA0huJLmI0qqAi+eH0bgUwciqmsC7/4d +6/6sCbg4BGBR3XISCisGAQQBl1UBBQEBB0ASLlXkJvhzAihOff7OLGQNlyD/+lCQ +1/qu/Ojr5GVbCwMBCAeIdQQYFgoAHQUCYFHdcgIbDAUWAgMBAAQLCQgHBRUKCQgL +Ah4BAAoJECFoxR5ec+rptOcA/iBkqa5nJNM03BDEyBaz87Ez28K5L8vltRwBUE/+ +LIrJAP43S3skfM1ErB/BPFv9YJzoTVaY5vESyEYoB6sw1H4iCA== +=wnXr +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: PGPainless + +mDMEYFHdchYJKwYBBAHaRw8BAQdA7qoKX7JDTjgbrnFKLfb43veznuC7z4KJ7acI +dXZI2om0E2ZyZXNoQGVuY3J5cHRlZC5rZXmIeAQTFgoAIAUCYFHdcgIbAwUWAgMB +AAQLCQgHBRUKCQgLAh4BAhkBAAoJEON4/e8A5cOx9c0BAPtneXwDhnhrtqG7JSue +0youMdGRCzKd8mfAkao/JAu+AP9TKRY6aqLQtwWUEhAUfE6TEic3x+WRlYAGBp7I +5XPpDLg4BGBR3XISCisGAQQBl1UBBQEBB0DSEcmogCWdMfhfmq7MlWuuzj9BuJo+ +qdrEvcSyAOolaAMBCAeIdQQYFgoAHQUCYFHdcgIbDAUWAgMBAAQLCQgHBRUKCQgL +Ah4BAAoJEON4/e8A5cOxpusA/iyib7m5aFuStRWlBs9MYfmEv3vbDGrDZoN+6wj0 +U3y7AQDskovK4Wd/vf2ljUOxP6knS01zriQpHy9ro5Qgi8bTCQ== +=p/wh +-----END PGP PUBLIC KEY BLOCK-----