diff --git a/src/main/java/org/pgpainless/pgpainless/util/KeyRingSubKeyFix.java b/src/main/java/org/pgpainless/pgpainless/util/KeyRingSubKeyFix.java new file mode 100644 index 00000000..0230ccfe --- /dev/null +++ b/src/main/java/org/pgpainless/pgpainless/util/KeyRingSubKeyFix.java @@ -0,0 +1,102 @@ +/* + * Copyright 2018 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.pgpainless.util; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.PublicSubkeyPacket; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PGPDigestCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; + +public class KeyRingSubKeyFix { + + private static final Logger LOGGER = Logger.getLogger(KeyRingSubKeyFix.class.getName()); + + /** + * This method makes sure, that sub keys do consist of sub key packets. + * Bouncycastle versions up to and including 1.60 created {@link PGPSecretKeyRing}s which sub keys consisted of + * normal public key packets, which would result in lost keys when converting PGPSecretKeyRings to PGPPublicKeyRings. + * + * This method throws a {@link RuntimeException} of a {@link NoSuchFieldException} or {@link IllegalAccessException}. + * + * @see Bouncycastle Java bug report #381 + * + * @param secretKeys possibly faulty PGPSecretKeyRing + * @param decryptor decryptor in case the keys are encrypted (can be null) + * @param encryptor encryptor to re-encrypt the keys in case they are encrypted (can be null) + * + * @return fixed PGPSecretKeyRing + * + * @throws PGPException in case we cannot dismantle or reassemble the key. + */ + public static PGPSecretKeyRing repairSubkeyPackets(PGPSecretKeyRing secretKeys, + PBESecretKeyDecryptor decryptor, + PBESecretKeyEncryptor encryptor) + throws PGPException { + List _secretKeys = new ArrayList<>(); + Iterator secretKeyIterator = secretKeys.iterator(); + _secretKeys.add(secretKeyIterator.next()); + + PGPDigestCalculator calculator = new BcPGPDigestCalculatorProvider().get(HashAlgorithmTags.SHA1); + + try { + + while (secretKeyIterator.hasNext()) { + PGPSecretKey subKey = secretKeyIterator.next(); + PGPPublicKey pubSubKey = subKey.getPublicKey(); + + // check for public key packet type + + Field publicPk = pubSubKey.getClass().getDeclaredField("publicPk"); + publicPk.setAccessible(true); + PublicKeyPacket keyPacket = (PublicKeyPacket) publicPk.get(pubSubKey); + + if (keyPacket instanceof PublicSubkeyPacket) { + // Sub key is already sub key + continue; + } + + // Sub key is normal key -> fix + LOGGER.log(Level.INFO, "Subkey " + Long.toHexString(subKey.getKeyID()) + " does not have a subkey key packet. Convert it..."); + keyPacket = new PublicSubkeyPacket(pubSubKey.getAlgorithm(), pubSubKey.getCreationTime(), keyPacket.getKey()); + publicPk.set(pubSubKey, keyPacket); + + PGPPrivateKey privateKey = subKey.extractPrivateKey(decryptor); + + PGPSecretKey secretKey = new PGPSecretKey(privateKey, pubSubKey, calculator, false, encryptor); + _secretKeys.add(secretKey); + } + + return new PGPSecretKeyRing(_secretKeys); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("Cannot apply fix due to an error while using reflections.", e); + } + } +} diff --git a/src/test/java/org/pgpainless/pgpainless/KeyRingSubKeyFixTest.java b/src/test/java/org/pgpainless/pgpainless/KeyRingSubKeyFixTest.java new file mode 100644 index 00000000..beb1688a --- /dev/null +++ b/src/test/java/org/pgpainless/pgpainless/KeyRingSubKeyFixTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2018 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.pgpainless; + +import static junit.framework.TestCase.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.util.Arrays; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.junit.Test; +import org.pgpainless.pgpainless.key.collection.PGPKeyRing; +import org.pgpainless.pgpainless.util.KeyRingSubKeyFix; + +public class KeyRingSubKeyFixTest { + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + @Test + public void test() + throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, + IOException { + PGPKeyRing ring = PGPainless.generateKeyRing().simpleEcKeyRing("hallo@welt.de"); + PGPSecretKeyRing secretKeys = ring.getSecretKeys(); + PGPPublicKeyRing publicKeys = ring.getPublicKeys(); + + PGPSecretKeyRing fixed = KeyRingSubKeyFix.repairSubkeyPackets(secretKeys, null, null); + PGPPublicKeyRing fixedPub = publicKeyRing(fixed); + + assertTrue(Arrays.equals(publicKeys.getEncoded(), fixedPub.getEncoded())); + } + + private PGPPublicKeyRing publicKeyRing(PGPSecretKeyRing secretKeys) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(2048); + for (PGPSecretKey s : secretKeys) { + PGPPublicKey p = s.getPublicKey(); + if (p != null) { + p.encode(buffer); + } + } + + return new PGPPublicKeyRing(buffer.toByteArray(), new BcKeyFingerprintCalculator()); + } +}