From d3fe850c9567ddd1d8f6e7d045df337a09e6d945 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 00:40:59 +0200 Subject: [PATCH] Initial implementation of 'change-key-password' command of SOP-07 --- .../secretkeyring/SecretKeyRingEditor.java | 99 +++---------------- .../org/pgpainless/key/util/KeyRingUtils.java | 71 +++++++++++++ .../pgpainless/sop/ChangeKeyPasswordImpl.java | 91 +++++++++++++++++ .../main/java/org/pgpainless/sop/SOPImpl.java | 6 ++ .../PGPainlessChangeKeyPasswordTest.java | 10 ++ 5 files changed, 193 insertions(+), 84 deletions(-) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 8f8e81fb..663c6e41 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -4,8 +4,21 @@ package org.pgpainless.key.modification.secretkeyring; -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.bcpg.SecretKeyPacket; +import static org.pgpainless.key.util.KeyRingUtils.changePassphrase; +import static org.pgpainless.util.CollectionUtils.concat; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + import org.bouncycastle.bcpg.sig.KeyExpirationTime; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; @@ -14,8 +27,6 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.AlgorithmSuite; import org.pgpainless.algorithm.CompressionAlgorithm; @@ -35,7 +46,6 @@ import org.pgpainless.key.protection.KeyRingProtectionSettings; import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; -import org.pgpainless.key.protection.fixes.S2KUsageFix; import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.RevocationAttributes; @@ -52,22 +62,6 @@ import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; import org.pgpainless.util.Passphrase; import org.pgpainless.util.selection.userid.SelectUserId; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; - -import static org.pgpainless.util.CollectionUtils.concat; - public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { private PGPSecretKeyRing secretKeyRing; @@ -813,67 +807,4 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } } - private PGPSecretKeyRing changePassphrase(Long keyId, - PGPSecretKeyRing secretKeys, - SecretKeyRingProtector oldProtector, - SecretKeyRingProtector newProtector) - throws PGPException { - List secretKeyList = new ArrayList<>(); - if (keyId == null) { - // change passphrase of whole key ring - Iterator secretKeyIterator = secretKeys.getSecretKeys(); - while (secretKeyIterator.hasNext()) { - PGPSecretKey secretKey = secretKeyIterator.next(); - secretKey = reencryptPrivateKey(secretKey, oldProtector, newProtector); - secretKeyList.add(secretKey); - } - } else { - // change passphrase of selected subkey only - Iterator secretKeyIterator = secretKeys.getSecretKeys(); - while (secretKeyIterator.hasNext()) { - PGPSecretKey secretKey = secretKeyIterator.next(); - if (secretKey.getPublicKey().getKeyID() == keyId) { - // Re-encrypt only the selected subkey - secretKey = reencryptPrivateKey(secretKey, oldProtector, newProtector); - } - secretKeyList.add(secretKey); - } - } - - PGPSecretKeyRing newRing = new PGPSecretKeyRing(secretKeyList); - newRing = s2kUsageFixIfNecessary(newRing, newProtector); - return newRing; - } - - private PGPSecretKeyRing s2kUsageFixIfNecessary(PGPSecretKeyRing secretKeys, SecretKeyRingProtector protector) - throws PGPException { - boolean hasS2KUsageChecksum = false; - for (PGPSecretKey secKey : secretKeys) { - if (secKey.getS2KUsage() == SecretKeyPacket.USAGE_CHECKSUM) { - hasS2KUsageChecksum = true; - break; - } - } - if (hasS2KUsageChecksum) { - secretKeys = S2KUsageFix.replaceUsageChecksumWithUsageSha1( - secretKeys, protector, true); - } - return secretKeys; - } - - private static PGPSecretKey reencryptPrivateKey( - PGPSecretKey secretKey, - SecretKeyRingProtector oldProtector, - SecretKeyRingProtector newProtector) - throws PGPException { - S2K s2k = secretKey.getS2K(); - // If the key uses GNU_DUMMY_S2K, we leave it as is and skip this block - if (s2k == null || s2k.getType() != S2K.GNU_DUMMY_S2K) { - long secretKeyId = secretKey.getKeyID(); - PBESecretKeyDecryptor decryptor = oldProtector.getDecryptor(secretKeyId); - PBESecretKeyEncryptor encryptor = newProtector.getEncryptor(secretKeyId); - secretKey = PGPSecretKey.copyWithNewPassword(secretKey, decryptor, encryptor); - } - return secretKey; - } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 4689535d..91de3be4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -14,6 +14,8 @@ import java.util.NoSuchElementException; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPPrivateKey; @@ -25,11 +27,14 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.util.Strings; import org.pgpainless.PGPainless; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.key.protection.fixes.S2KUsageFix; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -515,4 +520,70 @@ public final class KeyRingUtils { } return userIds; } + + public static PGPSecretKeyRing changePassphrase(Long keyId, + PGPSecretKeyRing secretKeys, + SecretKeyRingProtector oldProtector, + SecretKeyRingProtector newProtector) + throws PGPException { + List secretKeyList = new ArrayList<>(); + if (keyId == null) { + // change passphrase of whole key ring + Iterator secretKeyIterator = secretKeys.getSecretKeys(); + while (secretKeyIterator.hasNext()) { + PGPSecretKey secretKey = secretKeyIterator.next(); + secretKey = KeyRingUtils.reencryptPrivateKey(secretKey, oldProtector, newProtector); + secretKeyList.add(secretKey); + } + } else { + // change passphrase of selected subkey only + Iterator secretKeyIterator = secretKeys.getSecretKeys(); + while (secretKeyIterator.hasNext()) { + PGPSecretKey secretKey = secretKeyIterator.next(); + if (secretKey.getPublicKey().getKeyID() == keyId) { + // Re-encrypt only the selected subkey + secretKey = KeyRingUtils.reencryptPrivateKey(secretKey, oldProtector, newProtector); + } + secretKeyList.add(secretKey); + } + } + + PGPSecretKeyRing newRing = new PGPSecretKeyRing(secretKeyList); + newRing = s2kUsageFixIfNecessary(newRing, newProtector); + return newRing; + } + + + public static PGPSecretKey reencryptPrivateKey( + PGPSecretKey secretKey, + SecretKeyRingProtector oldProtector, + SecretKeyRingProtector newProtector) + throws PGPException { + S2K s2k = secretKey.getS2K(); + // If the key uses GNU_DUMMY_S2K, we leave it as is and skip this block + if (s2k == null || s2k.getType() != S2K.GNU_DUMMY_S2K) { + long secretKeyId = secretKey.getKeyID(); + PBESecretKeyDecryptor decryptor = oldProtector.getDecryptor(secretKeyId); + PBESecretKeyEncryptor encryptor = newProtector.getEncryptor(secretKeyId); + secretKey = PGPSecretKey.copyWithNewPassword(secretKey, decryptor, encryptor); + } + return secretKey; + } + + + public static PGPSecretKeyRing s2kUsageFixIfNecessary(PGPSecretKeyRing secretKeys, SecretKeyRingProtector protector) + throws PGPException { + boolean hasS2KUsageChecksum = false; + for (PGPSecretKey secKey : secretKeys) { + if (secKey.getS2KUsage() == SecretKeyPacket.USAGE_CHECKSUM) { + hasS2KUsageChecksum = true; + break; + } + } + if (hasS2KUsageChecksum) { + secretKeys = S2KUsageFix.replaceUsageChecksumWithUsageSha1( + secretKeys, protector, true); + } + return secretKeys; + } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java new file mode 100644 index 00000000..7f041e39 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.pgpainless.PGPainless; +import org.pgpainless.exception.MissingPassphraseException; +import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.util.ArmoredOutputStreamFactory; +import org.pgpainless.util.Passphrase; +import sop.Ready; +import sop.exception.SOPGPException; +import sop.operation.ChangeKeyPassword; + +public class ChangeKeyPasswordImpl implements ChangeKeyPassword { + + private final MatchMakingSecretKeyRingProtector oldProtector = new MatchMakingSecretKeyRingProtector(); + private Passphrase newPassphrase = Passphrase.emptyPassphrase(); + private boolean armor = true; + + @Override + public ChangeKeyPassword noArmor() { + armor = false; + return this; + } + + @Override + public ChangeKeyPassword oldKeyPassphrase(String oldPassphrase) { + oldProtector.addPassphrase(Passphrase.fromPassword(oldPassphrase)); + return this; + } + + @Override + public ChangeKeyPassword newKeyPassphrase(String newPassphrase) { + this.newPassphrase = Passphrase.fromPassword(newPassphrase); + return this; + } + + @Override + public Ready keys(InputStream inputStream) throws SOPGPException.KeyIsProtected { + SecretKeyRingProtector newProtector = SecretKeyRingProtector.unlockAnyKeyWith(newPassphrase); + PGPSecretKeyRingCollection secretKeyRingCollection; + try { + secretKeyRingCollection = PGPainless.readKeyRing().secretKeyRingCollection(inputStream); + } catch (IOException e) { + throw new SOPGPException.BadData(e); + } + + List updatedSecretKeys = new ArrayList<>(); + for (PGPSecretKeyRing secretKeys : secretKeyRingCollection) { + oldProtector.addSecretKey(secretKeys); + try { + PGPSecretKeyRing changed = KeyRingUtils.changePassphrase(null, secretKeys, oldProtector, newProtector); + updatedSecretKeys.add(changed); + } catch (MissingPassphraseException e) { + throw new SOPGPException.KeyIsProtected("Cannot unlock key " + OpenPgpFingerprint.of(secretKeys), e); + } catch (PGPException e) { + if (e.getMessage().contains("Exception decrypting key")) { + throw new SOPGPException.KeyIsProtected("Cannot unlock key " + OpenPgpFingerprint.of(secretKeys), e); + } + throw new RuntimeException("Cannot change passphrase of key " + OpenPgpFingerprint.of(secretKeys), e); + } + } + final PGPSecretKeyRingCollection changedSecretKeyCollection = new PGPSecretKeyRingCollection(updatedSecretKeys); + return new Ready() { + @Override + public void writeTo(OutputStream outputStream) throws IOException { + if (armor) { + ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(outputStream); + changedSecretKeyCollection.encode(armorOut); + armorOut.close(); + } else { + changedSecretKeyCollection.encode(outputStream); + } + } + }; + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index f2c6120b..4009d2a8 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -7,6 +7,7 @@ package org.pgpainless.sop; import org.pgpainless.util.ArmoredOutputStreamFactory; import sop.SOP; import sop.operation.Armor; +import sop.operation.ChangeKeyPassword; import sop.operation.Dearmor; import sop.operation.Decrypt; import sop.operation.DetachedSign; @@ -108,6 +109,11 @@ public class SOPImpl implements SOP { return new RevokeKeyImpl(); } + @Override + public ChangeKeyPassword changeKeyPassword() { + return new ChangeKeyPasswordImpl(); + } + @Override public InlineDetach inlineDetach() { return new InlineDetachImpl(); diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java new file mode 100644 index 00000000..c4c693ff --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.ChangeKeyPasswordTest; + +public class PGPainlessChangeKeyPasswordTest extends ChangeKeyPasswordTest { +}