diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/KeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/KeyRingEditor.java index 628363df..4fb277fb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/KeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/KeyRingEditor.java @@ -16,8 +16,10 @@ package org.pgpainless.key.modification; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -29,6 +31,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.openpgp.operator.PGPDigestCalculator; import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; @@ -37,7 +40,11 @@ import org.pgpainless.algorithm.SignatureType; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.protection.KeyRingProtectionSettings; +import org.pgpainless.key.protection.PassphraseMapKeyRingProtector; +import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnprotectedKeysProtector; +import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider; import org.pgpainless.key.util.OpenPgpKeyAttributeUtil; import org.pgpainless.util.Passphrase; @@ -100,12 +107,14 @@ public class KeyRingEditor implements KeyRingEditorInterface { return preferredHashAlgorithms.get(0); } + // TODO: Move to utility class private PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, SecretKeyRingProtector protector) throws PGPException { PBESecretKeyDecryptor secretKeyDecryptor = protector.getDecryptor(secretKey.getKeyID()); PGPPrivateKey privateKey = secretKey.extractPrivateKey(secretKeyDecryptor); return privateKey; } + // TODO: Move to utility class? private String sanitizeUserId(String userId) { userId = userId.trim(); // TODO: Further research how to sanitize user IDs. @@ -146,14 +155,22 @@ public class KeyRingEditor implements KeyRingEditorInterface { @Override public WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase(@Nullable Passphrase oldPassphrase, @Nonnull KeyRingProtectionSettings oldProtectionSettings) { - return new WithKeyRingEncryptionSettingsImpl(); + SecretKeyRingProtector protector = new PasswordBasedSecretKeyRingProtector( + oldProtectionSettings, + new SolitaryPassphraseProvider(oldPassphrase)); + + return new WithKeyRingEncryptionSettingsImpl(null, protector); } @Override public WithKeyRingEncryptionSettings changeSubKeyPassphraseFromOldPassphrase(@Nonnull Long keyId, @Nullable Passphrase oldPassphrase, @Nonnull KeyRingProtectionSettings oldProtectionSettings) { - return new WithKeyRingEncryptionSettingsImpl(); + Map passphraseMap = Collections.singletonMap(keyId, oldPassphrase); + SecretKeyRingProtector protector = new PassphraseMapKeyRingProtector( + passphraseMap, oldProtectionSettings, null); + + return new WithKeyRingEncryptionSettingsImpl(keyId, protector); } @Override @@ -161,7 +178,16 @@ public class KeyRingEditor implements KeyRingEditorInterface { return secretKeyRing; } - private class WithKeyRingEncryptionSettingsImpl implements WithKeyRingEncryptionSettings { + private final class WithKeyRingEncryptionSettingsImpl implements WithKeyRingEncryptionSettings { + + private final Long keyId; + // Protector to unlock the key with the old passphrase + private final SecretKeyRingProtector oldProtector; + + private WithKeyRingEncryptionSettingsImpl(Long keyId, SecretKeyRingProtector oldProtector) { + this.keyId = keyId; + this.oldProtector = oldProtector; + } @Override public WithPassphrase withSecureDefaultSettings() { @@ -170,20 +196,85 @@ public class KeyRingEditor implements KeyRingEditorInterface { @Override public WithPassphrase withCustomSettings(KeyRingProtectionSettings settings) { - return new WithPassphraseImpl(); + return new WithPassphraseImpl(keyId, oldProtector, settings); } } - private class WithPassphraseImpl implements WithPassphrase { + private final class WithPassphraseImpl implements WithPassphrase { + + private final SecretKeyRingProtector oldProtector; + private final KeyRingProtectionSettings newProtectionSettings; + private final Long keyId; + + private WithPassphraseImpl(Long keyId, SecretKeyRingProtector oldProtector, KeyRingProtectionSettings newProtectionSettings) { + this.keyId = keyId; + this.oldProtector = oldProtector; + this.newProtectionSettings = newProtectionSettings; + } @Override - public KeyRingEditorInterface toNewPassphrase(Passphrase passphrase) { + public KeyRingEditorInterface toNewPassphrase(Passphrase passphrase) throws PGPException { + SecretKeyRingProtector newProtector = new PasswordBasedSecretKeyRingProtector( + newProtectionSettings, new SolitaryPassphraseProvider(passphrase)); + + PGPSecretKeyRing secretKeys = changePassphrase(keyId, KeyRingEditor.this.secretKeyRing, oldProtector, newProtector); + KeyRingEditor.this.secretKeyRing = secretKeys; + return KeyRingEditor.this; } @Override - public KeyRingEditorInterface noPassphrase() { + public KeyRingEditorInterface noPassphrase() throws PGPException { + SecretKeyRingProtector newProtector = new UnprotectedKeysProtector(); + + PGPSecretKeyRing secretKeys = changePassphrase(keyId, KeyRingEditor.this.secretKeyRing, oldProtector, newProtector); + KeyRingEditor.this.secretKeyRing = secretKeys; + return KeyRingEditor.this; } + + private PGPSecretKeyRing changePassphrase(Long keyId, + PGPSecretKeyRing secretKeys, + SecretKeyRingProtector oldProtector, + SecretKeyRingProtector newProtector) throws PGPException { + if (keyId == null) { + // change passphrase of whole key ring + List newlyEncryptedSecretKeys = new ArrayList<>(); + Iterator secretKeyIterator = secretKeys.getSecretKeys(); + while (secretKeyIterator.hasNext()) { + PGPSecretKey secretKey = secretKeyIterator.next(); + PGPPrivateKey privateKey = unlockSecretKey(secretKey, oldProtector); + secretKey = lockPrivateKey(privateKey, secretKey.getPublicKey(), newProtector); + newlyEncryptedSecretKeys.add(secretKey); + } + return new PGPSecretKeyRing(newlyEncryptedSecretKeys); + } else { + // change passphrase of selected subkey only + List secretKeyList = new ArrayList<>(); + Iterator secretKeyIterator = secretKeys.getSecretKeys(); + while (secretKeyIterator.hasNext()) { + PGPSecretKey secretKey = secretKeyIterator.next(); + + if (secretKey.getPublicKey().getKeyID() == keyId) { + // Re-encrypt only the selected subkey + PGPPrivateKey privateKey = unlockSecretKey(secretKey, oldProtector); + secretKey = lockPrivateKey(privateKey, secretKey.getPublicKey(), newProtector); + } + + secretKeyList.add(secretKey); + } + return new PGPSecretKeyRing(secretKeyList); + } + } + + // TODO: Move to utility class + private PGPSecretKey lockPrivateKey(PGPPrivateKey privateKey, PGPPublicKey publicKey, SecretKeyRingProtector protector) throws PGPException { + PGPDigestCalculator checksumCalculator = new BcPGPDigestCalculatorProvider() + // TODO: Again, SHA1? + .get(HashAlgorithm.SHA1.getAlgorithmId()); + PBESecretKeyEncryptor encryptor = protector.getEncryptor(publicKey.getKeyID()); + PGPSecretKey secretKey = new PGPSecretKey(privateKey, publicKey, checksumCalculator, publicKey.isMasterKey(), encryptor); + return secretKey; + } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/KeyRingEditorInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/KeyRingEditorInterface.java index 332c54ec..806db3c0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/KeyRingEditorInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/KeyRingEditorInterface.java @@ -153,14 +153,14 @@ public interface KeyRingEditorInterface { * @param passphrase passphrase * @return editor builder */ - KeyRingEditorInterface toNewPassphrase(Passphrase passphrase); + KeyRingEditorInterface toNewPassphrase(Passphrase passphrase) throws PGPException; /** * Leave the key unprotected. * * @return editor builder */ - KeyRingEditorInterface noPassphrase(); + KeyRingEditorInterface noPassphrase() throws PGPException; } /** diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java index af793c65..a6d861a5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java @@ -18,7 +18,6 @@ package org.pgpainless.key.modification; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Iterator; @@ -35,7 +34,7 @@ import org.pgpainless.util.Passphrase; public class AddUserIdTest { @Test - public void addUserIdToExistingKeyRing() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { + public void addUserIdToExistingKeyRing() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { PGPKeyRing keyRing = PGPainless.generateKeyRing().simpleEcKeyRing("alice@wonderland.lit", "rabb1th0le"); PGPSecretKeyRing secretKeys = keyRing.getSecretKeys(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java index 6d09ff04..25fac950 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java @@ -36,7 +36,7 @@ import org.pgpainless.util.Passphrase; public class ChangeSecretKeyRingPassphraseTest { @Test - public void changePassphraseTest() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { + public void changePassphraseOfWholeKeyRingTest() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { PGPKeyRing keyRing = PGPainless.generateKeyRing().simpleEcKeyRing("password@encryp.ted", "weakPassphrase"); PGPSecretKeyRing secretKeys = PGPainless.modifyKeyRing(keyRing.getSecretKeys()) .changePassphraseFromOldPassphrase(Passphrase.fromPassword("weakPassphrase")) @@ -62,13 +62,13 @@ public class ChangeSecretKeyRingPassphraseTest { private void signDummyMessageWithKeysAndPassphrase(PGPKeyRing keyRing, Passphrase passphrase) throws IOException, PGPException { String dummyMessage = "dummy"; - ByteArrayOutputStream dummy = new ByteArrayOutputStream(); - EncryptionStream stream = PGPainless.createEncryptor().onOutputStream(dummy) - .doNotEncrypt() - .signWith(PasswordBasedSecretKeyRingProtector.forKey(keyRing.getSecretKeys(), passphrase), keyRing.getSecretKeys()) - .noArmor(); + ByteArrayOutputStream dummy = new ByteArrayOutputStream(); + EncryptionStream stream = PGPainless.createEncryptor().onOutputStream(dummy) + .doNotEncrypt() + .signWith(PasswordBasedSecretKeyRingProtector.forKey(keyRing.getSecretKeys(), passphrase), keyRing.getSecretKeys()) + .noArmor(); - Streams.pipeAll(new ByteArrayInputStream(dummyMessage.getBytes()), stream); - stream.close(); + Streams.pipeAll(new ByteArrayInputStream(dummyMessage.getBytes()), stream); + stream.close(); } }