diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java index 5ea3d0e6..d3881ea0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java @@ -29,7 +29,12 @@ import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProv import org.pgpainless.util.Passphrase; /** - * Interface that is used to provide secret key ring encryptors and decryptors. + * Task of the {@link SecretKeyRingProtector} is to map encryptor/decryptor objects to key-ids. + * {@link PBESecretKeyEncryptor PBESecretKeyEncryptors}/{@link PBESecretKeyDecryptor PBESecretKeyDecryptors} are used + * to encrypt/decrypt secret keys using a passphrase. + * + * While it is easy to create an implementation of this interface that fits your needs, there are a bunch of + * implementations ready for use. */ public interface SecretKeyRingProtector { @@ -57,6 +62,8 @@ public interface SecretKeyRingProtector { * The protector maintains an in-memory cache of passphrases and can be extended with new passphrases * at runtime. * + * See {@link CachingSecretKeyRingProtector} for how to memorize/forget additional passphrases during runtime. + * * @param missingPassphraseCallback callback that is used to provide missing passphrases. * @return caching secret key protector */ @@ -70,6 +77,9 @@ public interface SecretKeyRingProtector { /** * Use the provided passphrase to lock/unlock all subkeys in the provided key ring. * + * This protector will use the provided passphrase to lock/unlock all subkeys present in the provided keys object. + * For other keys that are not present in the ring, it will return null. + * * @param passphrase passphrase * @param keys key ring * @return protector @@ -84,6 +94,10 @@ public interface SecretKeyRingProtector { /** * Use the provided passphrase to lock/unlock only the provided (sub-)key. + * This protector will only return a non-null encryptor/decryptor based on the provided passphrase if + * {@link #getEncryptor(Long)}/{@link #getDecryptor(Long)} is getting called with the key-id of the provided key. + * + * Otherwise this protector will always return null. * * @param passphrase passphrase * @param key key to lock/unlock @@ -95,6 +109,12 @@ public interface SecretKeyRingProtector { /** * Protector for unprotected keys. + * This protector returns null for all {@link #getEncryptor(Long)}/{@link #getDecryptor(Long)} calls, + * no matter what the key-id is. + * + * As a consequence, this protector can only "unlock" keys which are not protected using a passphrase, and it will + * leave keys unprotected, should it be used to "protect" a key + * (eg. in {@link org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor#changePassphraseFromOldPassphrase(Passphrase)}). * * @return protector */ diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java index 03a70071..e9e65a0f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java @@ -16,6 +16,7 @@ package org.pgpainless.example; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; @@ -26,6 +27,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.AlgorithmSuite; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.Feature; @@ -34,6 +36,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.KeySpecBuilderInterface; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; import org.pgpainless.key.generation.type.eddsa.EdDSACurve; @@ -136,9 +139,11 @@ public class GenerateKeys { */ @Test public void generateSimpleECKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + // Define a primary user-id String userId = "mhelms@pgpainless.org"; + // Set a password to protect the secret key String password = "tr4ns"; - + // Generate the OpenPGP key PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() .simpleEcKeyRing(userId, password); @@ -153,6 +158,37 @@ public class GenerateKeys { * Among user-id and password, the user can add an arbitrary number of subkeys and specify their algorithms and * algorithm preferences. * + * If the target key amalgamation (key ring) should consist of more than just a single (sub-)key, start by providing + * the specifications for the subkeys first (in {@link org.pgpainless.key.generation.KeyRingBuilderInterface#withSubKey(KeySpec)}) + * and add the primary key specification last (in {@link org.pgpainless.key.generation.KeyRingBuilderInterface#withPrimaryKey(KeySpec)}. + * + * {@link KeySpec} objects can best be obtained by using the {@link KeySpec#getBuilder(KeyType)} method and providing a {@link KeyType}. + * There are a bunch of factory methods for different {@link KeyType} implementations present in {@link KeyType} itself + * (such as {@link KeyType#ECDH(EllipticCurve)}. + * + * After that, the {@link org.pgpainless.key.generation.KeySpecBuilder} needs to be further configured. + * First of all, the keys {@link KeyFlag KeyFlags} need to be specified. {@link KeyFlag KeyFlags} determine + * the use of the key, like encryption, signing data or certifying subkeys. + * KeyFlags can be set with {@link org.pgpainless.key.generation.KeySpecBuilder#withKeyFlags(KeyFlag...)}. + * + * Next is algorithm setup. You can either trust PGPainless' defaults (see {@link AlgorithmSuite#getDefaultAlgorithmSuite()}), + * or specify your own algorithm preferences. + * To go with the defaults, call {@link KeySpecBuilderInterface.WithDetailedConfiguration#withDefaultAlgorithms()}, + * otherwise start detailed config with {@link KeySpecBuilderInterface.WithDetailedConfiguration#withDetailedConfiguration()}. + * + * Note, that if you set preferred algorithms, the preference lists are sorted from high priority to low priority. + * + * When setting the primary key spec ({@link org.pgpainless.key.generation.KeyRingBuilder#withPrimaryKey(KeySpec)}), + * make sure that the primary key spec has the {@link KeyFlag} {@link KeyFlag#CERTIFY_OTHER} set, as this is an requirement + * for primary keys. + * + * Furthermore you have to set at least the primary user-id via + * {@link org.pgpainless.key.generation.KeyRingBuilderInterface.WithPrimaryUserId#withPrimaryUserId(String)}, + * but you can also add additional user-ids via + * {@link org.pgpainless.key.generation.KeyRingBuilderInterface.WithAdditionalUserIdOrPassphrase#withAdditionalUserId(String)}. + * + * Lastly you can decide whether or not to set a passphrase to protect the secret key. + * * @throws PGPException * @throws InvalidAlgorithmParameterException * @throws NoSuchAlgorithmException @@ -166,6 +202,7 @@ public class GenerateKeys { .withEmail("mcarpenter@pgpainless.org") .withComment("Pride!") .build(); + String additionalUserId = "mcarpenter@christopher.street"; // It is recommended to use the Passphrase class, as it can be used to safely invalidate passwords from memory Passphrase passphrase = Passphrase.fromPassword("1nters3x"); @@ -204,10 +241,14 @@ public class GenerateKeys { .withPrimaryKey( KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519)) // The primary key MUST carry the CERTIFY_OTHER flag, but CAN carry additional flags - .withKeyFlags(KeyFlag.CERTIFY_OTHER) - .withDefaultAlgorithms() + .withKeyFlags(KeyFlag.CERTIFY_OTHER) + .withDefaultAlgorithms() ) + // Set primary user-id .withPrimaryUserId(userId) + // Add an additional user id. This step can be repeated + .withAdditionalUserId(additionalUserId) + // Set passphrase. Alternatively use .withoutPassphrase() to leave key unprotected. .withPassphrase(passphrase) .build(); @@ -215,6 +256,7 @@ public class GenerateKeys { KeyRingInfo keyInfo = new KeyRingInfo(secretKey); assertEquals(3, keyInfo.getSecretKeys().size()); assertEquals("Morgan Carpenter (Pride!) ", keyInfo.getPrimaryUserId()); + assertTrue(keyInfo.isUserIdValid(additionalUserId)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java new file mode 100644 index 00000000..e0e8c7fe --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java @@ -0,0 +1,132 @@ +package org.pgpainless.example; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.exception.WrongPassphraseException; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.ecc.EllipticCurve; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; + +public class ModifyKeys { + + private final String userId = "alice@pgpainless.org"; + private final String originalPassphrase = "p4ssw0rd"; + private PGPSecretKeyRing secretKey; + private long primaryKeyId; + private long encryptionSubkeyId; + private long signingSubkeyId; + + @BeforeEach + public void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + secretKey = PGPainless.generateKeyRing() + .modernKeyRing(userId, originalPassphrase); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + primaryKeyId = info.getKeyId(); + encryptionSubkeyId = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS).get(0).getKeyID(); + signingSubkeyId = info.getSigningSubkeys().get(0).getKeyID(); + } + + /** + * This example demonstrates how to change the passphrase of a secret key and all its subkeys. + * + * @throws PGPException + */ + @Test + public void changePassphrase() throws PGPException { + secretKey = PGPainless.modifyKeyRing(secretKey) + .changePassphraseFromOldPassphrase(Passphrase.fromPassword(originalPassphrase)) + .withSecureDefaultSettings() + .toNewPassphrase(Passphrase.fromPassword("n3wP4ssW0rD")) + .done(); + + + // Old passphrase no longer works + assertThrows(WrongPassphraseException.class, () -> + UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(), Passphrase.fromPassword(originalPassphrase))); + // But the new one does + UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(), Passphrase.fromPassword("n3wP4ssW0rD")); + } + + /** + * This example demonstrates how to change the passphrase of a single subkey in a key to a new passphrase. + * Only the passphrase of the targeted key will be changed. All other keys remain untouched. + * + * @throws PGPException + */ + @Test + public void changeSingleSubkeyPassphrase() throws PGPException { + secretKey = PGPainless.modifyKeyRing(secretKey) + // Here we change the passphrase of the encryption subkey + .changeSubKeyPassphraseFromOldPassphrase(encryptionSubkeyId, Passphrase.fromPassword(originalPassphrase)) + .withSecureDefaultSettings() + .toNewPassphrase(Passphrase.fromPassword("cryptP4ssphr4s3")) + .done(); + + + // encryption key can now only be unlocked using the new passphrase + assertThrows(WrongPassphraseException.class, () -> + UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(encryptionSubkeyId), Passphrase.fromPassword(originalPassphrase))); + UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(encryptionSubkeyId), Passphrase.fromPassword("cryptP4ssphr4s3")); + // primary key remains unchanged + UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(primaryKeyId), Passphrase.fromPassword(originalPassphrase)); + } + + /** + * This example demonstrates how to add an additional user-id to a key. + * + * @throws PGPException + */ + @Test + public void addUserId() throws PGPException { + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAllKeysWith(Passphrase.fromPassword(originalPassphrase), secretKey); + secretKey = PGPainless.modifyKeyRing(secretKey) + .addUserId("additional@user.id", protector) + .done(); + + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + assertTrue(info.isUserIdValid("additional@user.id")); + assertFalse(info.isUserIdValid("another@user.id")); + } + + @Test + public void addSubkey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + // Protector for unlocking the existing secret key + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAllKeysWith(Passphrase.fromPassword(originalPassphrase), secretKey); + Passphrase subkeyPassphrase = Passphrase.fromPassword("subk3yP4ssphr4s3"); + assertEquals(1, new KeyRingInfo(secretKey).getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS).size()); + secretKey = PGPainless.modifyKeyRing(secretKey) + .addSubKey( + KeySpec.getBuilder(KeyType.ECDH(EllipticCurve._BRAINPOOLP512R1)) + .withKeyFlags(KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE) + .withDefaultAlgorithms(), + subkeyPassphrase, + protector) + .done(); + + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + assertEquals(4, info.getSecretKeys().size()); + assertEquals(4, info.getPublicKeys().size()); + assertEquals(2, info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS).size()); + + } +}