From 562aff5f9df8d2ecf8e3a4b1450948a1006e8289 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 13 Sep 2023 15:05:58 +0200 Subject: [PATCH] Kotlin conversion: SecretKeyRingEditor --- .../key/modification/package-info.java | 8 - .../secretkeyring/SecretKeyRingEditor.java | 810 ------------------ .../SecretKeyRingEditorInterface.java | 652 -------------- .../secretkeyring/package-info.java | 8 - .../extensions/PGPKeyRingExtensions.kt | 7 + .../org/pgpainless/key/info/KeyRingInfo.kt | 86 +- .../secretkeyring/SecretKeyRingEditor.kt | 493 +++++++++++ .../SecretKeyRingEditorInterface.kt | 560 ++++++++++++ .../pgpainless/key/info/KeyRingInfoTest.java | 2 +- .../key/modification/AddUserIdTest.java | 7 +- .../key/protection/fixes/S2KUsageFixTest.java | 2 +- 11 files changed, 1110 insertions(+), 1525 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/modification/package-info.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/package-info.java deleted file mode 100644 index 9fa73e88..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes that deal with modifications made to OpenPGP keys. - */ -package org.pgpainless.key.modification; 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 deleted file mode 100644 index 663c6e41..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ /dev/null @@ -1,810 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.modification.secretkeyring; - -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; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.AlgorithmSuite; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.Feature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.generation.KeyRingBuilder; -import org.pgpainless.key.generation.KeySpec; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.CachingSecretKeyRingProtector; -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.passphrase_provider.SolitaryPassphraseProvider; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.signature.builder.DirectKeySelfSignatureBuilder; -import org.pgpainless.signature.builder.PrimaryKeyBindingSignatureBuilder; -import org.pgpainless.signature.builder.RevocationSignatureBuilder; -import org.pgpainless.signature.builder.SelfSignatureBuilder; -import org.pgpainless.signature.builder.SubkeyBindingSignatureBuilder; -import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.selection.userid.SelectUserId; - -public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { - - private PGPSecretKeyRing secretKeyRing; - private final Date referenceTime; - - public SecretKeyRingEditor(@Nonnull PGPSecretKeyRing secretKeyRing) { - this(secretKeyRing, new Date()); - } - - public SecretKeyRingEditor(@Nonnull PGPSecretKeyRing secretKeyRing, - @Nonnull Date referenceTime) { - this.secretKeyRing = secretKeyRing; - this.referenceTime = referenceTime; - } - - @Nonnull - @Override - public Date getReferenceTime() { - return referenceTime; - } - - @Override - public SecretKeyRingEditorInterface addUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return addUserId(userId, null, secretKeyRingProtector); - } - - @Override - public SecretKeyRingEditorInterface addUserId( - @Nonnull CharSequence userId, - @Nullable SelfSignatureSubpackets.Callback signatureSubpacketCallback, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - String sanitizeUserId = sanitizeUserId(userId); - - // user-id certifications live on the primary key - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - - // retain key flags from previous signature - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - if (info.isHardRevoked(userId.toString())) { - throw new IllegalArgumentException("User-ID " + userId + " is hard revoked and cannot be re-certified."); - } - List keyFlags = info.getKeyFlagsOf(info.getKeyId()); - - Set hashAlgorithmPreferences; - Set symmetricKeyAlgorithmPreferences; - Set compressionAlgorithmPreferences; - try { - hashAlgorithmPreferences = info.getPreferredHashAlgorithms(); - symmetricKeyAlgorithmPreferences = info.getPreferredSymmetricKeyAlgorithms(); - compressionAlgorithmPreferences = info.getPreferredCompressionAlgorithms(); - } catch (IllegalStateException e) { - // missing user-id sig - AlgorithmSuite algorithmSuite = AlgorithmSuite.getDefaultAlgorithmSuite(); - hashAlgorithmPreferences = algorithmSuite.getHashAlgorithms(); - symmetricKeyAlgorithmPreferences = algorithmSuite.getSymmetricKeyAlgorithms(); - compressionAlgorithmPreferences = algorithmSuite.getCompressionAlgorithms(); - } - - SelfSignatureBuilder builder = new SelfSignatureBuilder(primaryKey, protector); - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - builder.setSignatureType(SignatureType.POSITIVE_CERTIFICATION); - - // Retain signature subpackets of previous signatures - builder.getHashedSubpackets().setKeyFlags(keyFlags); - builder.getHashedSubpackets().setPreferredHashAlgorithms(hashAlgorithmPreferences); - builder.getHashedSubpackets().setPreferredSymmetricKeyAlgorithms(symmetricKeyAlgorithmPreferences); - builder.getHashedSubpackets().setPreferredCompressionAlgorithms(compressionAlgorithmPreferences); - builder.getHashedSubpackets().setFeatures(Feature.MODIFICATION_DETECTION); - - builder.applyCallback(signatureSubpacketCallback); - - PGPSignature signature = builder.build(primaryKey.getPublicKey(), sanitizeUserId); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, sanitizeUserId, signature); - - return this; - } - - @Override - public SecretKeyRingEditorInterface addPrimaryUserId( - @Nonnull CharSequence userId, @Nonnull SecretKeyRingProtector protector) - throws PGPException { - - // Determine previous key expiration date - PGPPublicKey primaryKey = secretKeyRing.getSecretKey().getPublicKey(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - String primaryUserId = info.getPrimaryUserId(); - PGPSignature signature = primaryUserId == null ? - info.getLatestDirectKeySelfSignature() : info.getLatestUserIdCertification(primaryUserId); - final Date previousKeyExpiration = signature == null ? null : - SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(signature, primaryKey); - - // Add new primary user-id signature - addUserId( - userId, - new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - hashedSubpackets.setPrimaryUserId(); - if (previousKeyExpiration != null) { - hashedSubpackets.setKeyExpirationTime(primaryKey, previousKeyExpiration); - } else { - hashedSubpackets.setKeyExpirationTime(null); - } - } - }, - protector); - - // unmark previous primary user-ids to be non-primary - info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - for (String otherUserId : info.getValidAndExpiredUserIds()) { - if (userId.toString().equals(otherUserId)) { - continue; - } - - // We need to unmark this user-id as primary - PGPSignature userIdCertification = info.getLatestUserIdCertification(otherUserId); - assert (userIdCertification != null); - - if (userIdCertification.getHashedSubPackets().isPrimaryUserID()) { - addUserId(otherUserId, new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - hashedSubpackets.setPrimaryUserId(null); - hashedSubpackets.setKeyExpirationTime(null); // non-primary - } - }, protector); - } - } - return this; - } - - @Override - public SecretKeyRingEditorInterface removeUserId( - SelectUserId userIdSelector, - SecretKeyRingProtector protector) - throws PGPException { - RevocationAttributes revocationAttributes = RevocationAttributes.createCertificateRevocation() - .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) - .withoutDescription(); - return revokeUserIds(userIdSelector, - protector, - revocationAttributes); - } - - @Override - public SecretKeyRingEditorInterface removeUserId( - CharSequence userId, - SecretKeyRingProtector protector) - throws PGPException { - return removeUserId( - SelectUserId.exactMatch(userId.toString()), - protector); - } - - @Override - public SecretKeyRingEditorInterface replaceUserId(@Nonnull CharSequence oldUserId, - @Nonnull CharSequence newUserId, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - String oldUID = oldUserId.toString().trim(); - String newUID = newUserId.toString().trim(); - if (oldUID.isEmpty()) { - throw new IllegalArgumentException("Old user-id cannot be empty."); - } - - if (newUID.isEmpty()) { - throw new IllegalArgumentException("New user-id cannot be empty."); - } - - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - if (!info.isUserIdValid(oldUID)) { - throw new NoSuchElementException("Key does not carry user-id '" + oldUID + "', or it is not valid."); - } - - PGPSignature oldCertification = info.getLatestUserIdCertification(oldUID); - if (oldCertification == null) { - throw new AssertionError("Certification for old user-id MUST NOT be null."); - } - - // Bind new user-id - addUserId(newUserId, new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - SignatureSubpacketsHelper.applyFrom(oldCertification.getHashedSubPackets(), (SignatureSubpackets) hashedSubpackets); - // Primary user-id - if (oldUID.equals(info.getPrimaryUserId())) { - // Implicit primary user-id - if (!oldCertification.getHashedSubPackets().isPrimaryUserID()) { - hashedSubpackets.setPrimaryUserId(); - } - } - } - - @Override - public void modifyUnhashedSubpackets(SelfSignatureSubpackets unhashedSubpackets) { - SignatureSubpacketsHelper.applyFrom(oldCertification.getUnhashedSubPackets(), (SignatureSubpackets) unhashedSubpackets); - } - }, protector); - - return revokeUserId(oldUID, protector); - } - - // TODO: Move to utility class? - private String sanitizeUserId(@Nonnull CharSequence userId) { - // TODO: Further research how to sanitize user IDs. - // eg. what about newlines? - return userId.toString().trim(); - } - - @Override - public SecretKeyRingEditorInterface addSubKey( - @Nonnull KeySpec keySpec, - @Nonnull Passphrase subKeyPassphrase, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(keySpec); - - SecretKeyRingProtector subKeyProtector = PasswordBasedSecretKeyRingProtector - .forKeyId(keyPair.getKeyID(), subKeyPassphrase); - - SelfSignatureSubpackets.Callback callback = new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - SignatureSubpacketsHelper.applyFrom(keySpec.getSubpackets(), (SignatureSubpackets) hashedSubpackets); - } - }; - - List keyFlags = KeyFlag.fromBitmask(keySpec.getSubpackets().getKeyFlags()); - KeyFlag firstFlag = keyFlags.remove(0); - KeyFlag[] otherFlags = keyFlags.toArray(new KeyFlag[0]); - - return addSubKey(keyPair, callback, subKeyProtector, secretKeyRingProtector, firstFlag, otherFlags); - } - - @Override - public SecretKeyRingEditorInterface addSubKey( - @Nonnull KeySpec keySpec, - @Nullable Passphrase subkeyPassphrase, - @Nullable SelfSignatureSubpackets.Callback subpacketsCallback, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(keySpec); - - SecretKeyRingProtector subKeyProtector = PasswordBasedSecretKeyRingProtector - .forKeyId(keyPair.getKeyID(), subkeyPassphrase); - - List keyFlags = KeyFlag.fromBitmask(keySpec.getSubpackets().getKeyFlags()); - KeyFlag firstFlag = keyFlags.remove(0); - KeyFlag[] otherFlags = keyFlags.toArray(new KeyFlag[0]); - - return addSubKey(keyPair, subpacketsCallback, subKeyProtector, secretKeyRingProtector, firstFlag, otherFlags); - } - - @Override - public SecretKeyRingEditorInterface addSubKey( - @Nonnull PGPKeyPair subkey, - @Nullable SelfSignatureSubpackets.Callback bindingSignatureCallback, - @Nonnull SecretKeyRingProtector subkeyProtector, - @Nonnull SecretKeyRingProtector primaryKeyProtector, - @Nonnull KeyFlag keyFlag, - KeyFlag... additionalKeyFlags) - throws PGPException, IOException { - KeyFlag[] flags = concat(keyFlag, additionalKeyFlags); - PublicKeyAlgorithm subkeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm()); - SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm); - - // check key against public key algorithm policy - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm()); - int bitStrength = subkey.getPublicKey().getBitStrength(); - if (!PGPainless.getPolicy().getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) { - throw new IllegalArgumentException("Public key algorithm policy violation: " + - publicKeyAlgorithm + " with bit strength " + bitStrength + " is not acceptable."); - } - - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - HashAlgorithm hashAlgorithm = HashAlgorithmNegotiator - .negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) - .negotiateHashAlgorithm(info.getPreferredHashAlgorithms()); - - PGPSecretKey secretSubkey = new PGPSecretKey(subkey.getPrivateKey(), subkey.getPublicKey(), ImplementationFactory.getInstance() - .getV4FingerprintCalculator(), false, subkeyProtector.getEncryptor(subkey.getKeyID())); - - SubkeyBindingSignatureBuilder skBindingBuilder = new SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector, hashAlgorithm); - skBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - skBindingBuilder.getHashedSubpackets().setKeyFlags(flags); - - if (subkeyAlgorithm.isSigningCapable()) { - PrimaryKeyBindingSignatureBuilder pkBindingBuilder = new PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm); - pkBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - PGPSignature pkBinding = pkBindingBuilder.build(primaryKey.getPublicKey()); - skBindingBuilder.getHashedSubpackets().addEmbeddedSignature(pkBinding); - } - - skBindingBuilder.applyCallback(bindingSignatureCallback); - PGPSignature skBinding = skBindingBuilder.build(secretSubkey.getPublicKey()); - - secretSubkey = KeyRingUtils.secretKeyPlusSignature(secretSubkey, skBinding); - secretKeyRing = KeyRingUtils.keysPlusSecretKey(secretKeyRing, secretSubkey); - return this; - } - - @Override - public SecretKeyRingEditorInterface revoke(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - RevocationSignatureSubpackets.Callback callback = callbackFromRevocationAttributes(revocationAttributes); - return revoke(secretKeyRingProtector, callback); - } - - @Override - public SecretKeyRingEditorInterface revoke(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException { - return revokeSubKey(secretKeyRing.getSecretKey().getKeyID(), secretKeyRingProtector, subpacketsCallback); - } - - @Override - public SecretKeyRingEditorInterface revokeSubKey(long subKeyId, - SecretKeyRingProtector protector, - RevocationAttributes revocationAttributes) - throws PGPException { - RevocationSignatureSubpackets.Callback callback = callbackFromRevocationAttributes(revocationAttributes); - return revokeSubKey(subKeyId, protector, callback); - } - - @Override - public SecretKeyRingEditorInterface revokeSubKey(long keyID, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException { - // retrieve subkey to be revoked - PGPPublicKey revokeeSubKey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, keyID); - // create revocation - PGPSignature subKeyRevocation = generateRevocation(secretKeyRingProtector, revokeeSubKey, - subpacketsCallback); - // inject revocation sig into key ring - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, revokeeSubKey, subKeyRevocation); - return this; - } - - @Override - public PGPSignature createRevocation(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - PGPPublicKey revokeeSubKey = secretKeyRing.getPublicKey(); - PGPSignature revocationCertificate = generateRevocation( - secretKeyRingProtector, revokeeSubKey, callbackFromRevocationAttributes(revocationAttributes)); - return revocationCertificate; - } - - @Override - public PGPSignature createRevocation( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - PGPPublicKey revokeeSubkey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, subkeyId); - RevocationSignatureSubpackets.Callback callback = callbackFromRevocationAttributes(revocationAttributes); - return generateRevocation(secretKeyRingProtector, revokeeSubkey, callback); - } - - @Override - public PGPSignature createRevocation( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback certificateSubpacketsCallback) - throws PGPException { - PGPPublicKey revokeeSubkey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, subkeyId); - return generateRevocation(secretKeyRingProtector, revokeeSubkey, certificateSubpacketsCallback); - } - - private PGPSignature generateRevocation(@Nonnull SecretKeyRingProtector protector, - @Nonnull PGPPublicKey revokeeSubKey, - @Nullable RevocationSignatureSubpackets.Callback callback) - throws PGPException { - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - SignatureType signatureType = revokeeSubKey.isMasterKey() ? - SignatureType.KEY_REVOCATION : SignatureType.SUBKEY_REVOCATION; - - RevocationSignatureBuilder signatureBuilder = - new RevocationSignatureBuilder(signatureType, primaryKey, protector); - signatureBuilder.applyCallback(callback); - PGPSignature revocation = signatureBuilder.build(revokeeSubKey); - return revocation; - } - - private static RevocationSignatureSubpackets.Callback callbackFromRevocationAttributes( - @Nullable RevocationAttributes attributes) { - return new RevocationSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) { - if (attributes != null) { - hashedSubpackets.setRevocationReason(attributes); - } - } - }; - } - - @Override - public SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - if (revocationAttributes != null) { - RevocationAttributes.Reason reason = revocationAttributes.getReason(); - if (reason != RevocationAttributes.Reason.NO_REASON - && reason != RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) { - throw new IllegalArgumentException("Revocation reason must either be NO_REASON or USER_ID_NO_LONGER_VALID"); - } - } - - RevocationSignatureSubpackets.Callback callback = new RevocationSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) { - if (revocationAttributes != null) { - hashedSubpackets.setRevocationReason(false, revocationAttributes); - } - } - }; - - return revokeUserId(userId, secretKeyRingProtector, callback); - } - - @Override - public SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketCallback) - throws PGPException { - String sanitized = sanitizeUserId(userId); - return revokeUserIds( - SelectUserId.exactMatch(sanitized), - secretKeyRingProtector, - subpacketCallback); - } - - @Override - public SecretKeyRingEditorInterface revokeUserIds( - @Nonnull SelectUserId userIdSelector, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - - return revokeUserIds( - userIdSelector, - secretKeyRingProtector, - new RevocationSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) { - hashedSubpackets.setRevocationReason(revocationAttributes); - } - }); - } - - @Override - public SecretKeyRingEditorInterface revokeUserIds( - @Nonnull SelectUserId userIdSelector, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException { - List selected = userIdSelector.selectUserIds(secretKeyRing); - if (selected.isEmpty()) { - throw new NoSuchElementException("No matching user-ids found on the key."); - } - - for (String userId : selected) { - doRevokeUserId(userId, secretKeyRingProtector, subpacketsCallback); - } - - return this; - } - - private SecretKeyRingEditorInterface doRevokeUserId( - @Nonnull String userId, - @Nonnull SecretKeyRingProtector protector, - @Nullable RevocationSignatureSubpackets.Callback callback) - throws PGPException { - PGPSecretKey primarySecretKey = secretKeyRing.getSecretKey(); - RevocationSignatureBuilder signatureBuilder = new RevocationSignatureBuilder( - SignatureType.CERTIFICATION_REVOCATION, - primarySecretKey, - protector); - if (referenceTime != null) { - signatureBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - - signatureBuilder.applyCallback(callback); - - PGPSignature revocationSignature = signatureBuilder.build(userId); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, userId, revocationSignature); - return this; - } - - @Override - public SecretKeyRingEditorInterface setExpirationDate( - @Nullable Date expiration, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - if (!primaryKey.isMasterKey()) { - throw new IllegalArgumentException("Key Ring does not appear to contain a primary secret key."); - } - - // reissue direct key sig - PGPSignature prevDirectKeySig = getPreviousDirectKeySignature(); - if (prevDirectKeySig != null) { - PGPSignature directKeySig = reissueDirectKeySignature(expiration, secretKeyRingProtector, prevDirectKeySig); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryKey.getPublicKey(), directKeySig); - } - - // reissue primary user-id sig - String primaryUserId = PGPainless.inspectKeyRing(secretKeyRing, referenceTime).getPossiblyExpiredPrimaryUserId(); - if (primaryUserId != null) { - PGPSignature prevUserIdSig = getPreviousUserIdSignatures(primaryUserId); - PGPSignature userIdSig = reissuePrimaryUserIdSig(expiration, secretKeyRingProtector, primaryUserId, prevUserIdSig); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryUserId, userIdSig); - } - - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - for (String userId : info.getValidUserIds()) { - if (userId.equals(primaryUserId)) { - continue; - } - - PGPSignature prevUserIdSig = info.getLatestUserIdCertification(userId); - if (prevUserIdSig == null) { - throw new AssertionError("A valid user-id shall never have no user-id signature."); - } - - if (prevUserIdSig.getHashedSubPackets().isPrimaryUserID()) { - assert (primaryUserId != null); - PGPSignature userIdSig = reissueNonPrimaryUserId(secretKeyRingProtector, userId, prevUserIdSig); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryUserId, userIdSig); - } - } - - return this; - } - - @Override - public PGPPublicKeyRing createMinimalRevocationCertificate( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes keyRevocationAttributes) - throws PGPException { - // Check reason - if (keyRevocationAttributes != null && !RevocationAttributes.Reason.isKeyRevocation(keyRevocationAttributes.getReason())) { - throw new IllegalArgumentException("Revocation reason MUST be applicable to a key revocation."); - } - - PGPSignature revocation = createRevocation(secretKeyRingProtector, keyRevocationAttributes); - PGPPublicKey primaryKey = secretKeyRing.getSecretKey().getPublicKey(); - primaryKey = KeyRingUtils.getStrippedDownPublicKey(primaryKey); - primaryKey = PGPPublicKey.addCertification(primaryKey, revocation); - return new PGPPublicKeyRing(Collections.singletonList(primaryKey)); - } - - private PGPSignature reissueNonPrimaryUserId( - SecretKeyRingProtector secretKeyRingProtector, - String userId, - PGPSignature prevUserIdSig) - throws PGPException { - SelfSignatureBuilder builder = new SelfSignatureBuilder(secretKeyRing.getSecretKey(), secretKeyRingProtector, prevUserIdSig); - if (referenceTime != null) { - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - builder.applyCallback(new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - // unmark as primary - hashedSubpackets.setPrimaryUserId(null); - } - }); - return builder.build(secretKeyRing.getPublicKey(), userId); - } - - private PGPSignature reissuePrimaryUserIdSig( - @Nullable Date expiration, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nonnull String primaryUserId, - @Nonnull PGPSignature prevUserIdSig) - throws PGPException { - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - PGPPublicKey publicKey = primaryKey.getPublicKey(); - - SelfSignatureBuilder builder = new SelfSignatureBuilder(primaryKey, secretKeyRingProtector, prevUserIdSig); - if (referenceTime != null) { - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - builder.applyCallback(new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - if (expiration != null) { - hashedSubpackets.setKeyExpirationTime(true, publicKey.getCreationTime(), expiration); - } else { - hashedSubpackets.setKeyExpirationTime(new KeyExpirationTime(true, 0)); - } - hashedSubpackets.setPrimaryUserId(); - } - }); - return builder.build(publicKey, primaryUserId); - } - - private PGPSignature reissueDirectKeySignature( - Date expiration, - SecretKeyRingProtector secretKeyRingProtector, - PGPSignature prevDirectKeySig) - throws PGPException { - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - PGPPublicKey publicKey = primaryKey.getPublicKey(); - final Date keyCreationTime = publicKey.getCreationTime(); - - DirectKeySelfSignatureBuilder builder = new DirectKeySelfSignatureBuilder(primaryKey, secretKeyRingProtector, prevDirectKeySig); - if (referenceTime != null) { - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - builder.applyCallback(new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - if (expiration != null) { - hashedSubpackets.setKeyExpirationTime(keyCreationTime, expiration); - } else { - hashedSubpackets.setKeyExpirationTime(null); - } - } - }); - - return builder.build(publicKey); - } - - private PGPSignature getPreviousDirectKeySignature() { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - return info.getLatestDirectKeySelfSignature(); - } - - private PGPSignature getPreviousUserIdSignatures(String userId) { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - return info.getLatestUserIdCertification(userId); - } - - @Override - public WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase( - @Nullable Passphrase oldPassphrase, - @Nonnull KeyRingProtectionSettings oldProtectionSettings) { - 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) { - Map passphraseMap = Collections.singletonMap(keyId, oldPassphrase); - SecretKeyRingProtector protector = new CachingSecretKeyRingProtector( - passphraseMap, oldProtectionSettings, null); - - return new WithKeyRingEncryptionSettingsImpl(keyId, protector); - } - - @Override - public PGPSecretKeyRing done() { - return secretKeyRing; - } - - private final class WithKeyRingEncryptionSettingsImpl implements WithKeyRingEncryptionSettings { - - private final Long keyId; - // Protector to unlock the key with the old passphrase - private final SecretKeyRingProtector oldProtector; - - /** - * Builder for selecting protection settings. - * - * If the keyId is null, the whole keyRing will get the same new passphrase. - * - * @param keyId id of the subkey whose passphrase will be changed, or null. - * @param oldProtector protector do unlock the key/ring. - */ - private WithKeyRingEncryptionSettingsImpl(Long keyId, SecretKeyRingProtector oldProtector) { - this.keyId = keyId; - this.oldProtector = oldProtector; - } - - @Override - public WithPassphrase withSecureDefaultSettings() { - return withCustomSettings(KeyRingProtectionSettings.secureDefaultSettings()); - } - - @Override - public WithPassphrase withCustomSettings(KeyRingProtectionSettings settings) { - return new WithPassphraseImpl(keyId, oldProtector, settings); - } - } - - 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 SecretKeyRingEditorInterface toNewPassphrase(Passphrase passphrase) - throws PGPException { - SecretKeyRingProtector newProtector = new PasswordBasedSecretKeyRingProtector( - newProtectionSettings, new SolitaryPassphraseProvider(passphrase)); - - PGPSecretKeyRing secretKeys = changePassphrase( - keyId, SecretKeyRingEditor.this.secretKeyRing, oldProtector, newProtector); - SecretKeyRingEditor.this.secretKeyRing = secretKeys; - - return SecretKeyRingEditor.this; - } - - @Override - public SecretKeyRingEditorInterface toNoPassphrase() - throws PGPException { - SecretKeyRingProtector newProtector = new UnprotectedKeysProtector(); - - PGPSecretKeyRing secretKeys = changePassphrase( - keyId, SecretKeyRingEditor.this.secretKeyRing, oldProtector, newProtector); - SecretKeyRingEditor.this.secretKeyRing = secretKeys; - - return SecretKeyRingEditor.this; - } - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java deleted file mode 100644 index dd7ed499..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java +++ /dev/null @@ -1,652 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.modification.secretkeyring; - -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Date; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.generation.KeySpec; -import org.pgpainless.key.protection.KeyRingProtectionSettings; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.selection.userid.SelectUserId; - -public interface SecretKeyRingEditorInterface { - - /** - * Return the editors reference time. - * - * @return reference time - */ - @Nonnull - Date getReferenceTime(); - - /** - * Add a user-id to the key ring. - * - * @param userId user-id - * @param secretKeyRingProtector protector to unlock the secret key - * @return the builder - * - * @throws PGPException in case we cannot generate a signature for the user-id - */ - SecretKeyRingEditorInterface addUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException; - - /** - * Add a user-id to the key ring. - * - * @param userId user-id - * @param signatureSubpacketCallback callback that can be used to modify signature subpackets of the - * certification signature. - * @param protector protector to unlock the primary secret key - * @return the builder - * - * @throws PGPException in case we cannot generate a signature for the user-id - */ - SecretKeyRingEditorInterface addUserId( - @Nonnull CharSequence userId, - @Nullable SelfSignatureSubpackets.Callback signatureSubpacketCallback, - @Nonnull SecretKeyRingProtector protector) - throws PGPException; - - /** - * Add a user-id to the key ring and mark it as primary. - * If the user-id is already present, a new certification signature will be created. - * - * @param userId user id - * @param protector protector to unlock the secret key - * @return the builder - * - * @throws PGPException in case we cannot generate a signature for the user-id - */ - SecretKeyRingEditorInterface addPrimaryUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector protector) - throws PGPException; - - /** - * Convenience method to revoke selected user-ids using soft revocation signatures. - * The revocation will use {@link RevocationAttributes.Reason#USER_ID_NO_LONGER_VALID}, so that the user-id - * can be re-certified at a later point. - * - * @param userIdSelector selector to select user-ids - * @param protector protector to unlock the primary key - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface removeUserId(SelectUserId userIdSelector, - SecretKeyRingProtector protector) - throws PGPException; - - /** - * Convenience method to revoke a single user-id using a soft revocation signature. - * The revocation will use {@link RevocationAttributes.Reason#USER_ID_NO_LONGER_VALID}. so that the user-id - * can be re-certified at a later point. - * - * @param userId user-id to revoke - * @param protector protector to unlock the primary key - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface removeUserId(CharSequence userId, - SecretKeyRingProtector protector) - throws PGPException; - - /** - * Replace a user-id on the key with a new one. - * The old user-id gets soft revoked and the new user-id gets bound with the same signature subpackets as the - * old one, with one exception: - * If the old user-id was implicitly primary (did not carry a {@link org.bouncycastle.bcpg.sig.PrimaryUserID} packet, - * but effectively was primary, then the new user-id will be explicitly marked as primary. - * - * @param oldUserId old user-id - * @param newUserId new user-id - * @param protector protector to unlock the secret key - * @return the builder - * @throws PGPException in case we cannot generate a revocation and certification signature - * @throws java.util.NoSuchElementException if the old user-id was not found on the key; or if the oldUserId - * was already invalid - */ - SecretKeyRingEditorInterface replaceUserId(CharSequence oldUserId, - CharSequence newUserId, - SecretKeyRingProtector protector) - throws PGPException; - - /** - * Add a subkey to the key ring. - * The subkey will be generated from the provided {@link KeySpec}. - * - * @param keySpec key specification - * @param subKeyPassphrase passphrase to encrypt the sub key - * @param secretKeyRingProtector protector to unlock the secret key of the key ring - * @return the builder - * - * @throws InvalidAlgorithmParameterException in case the user wants to use invalid parameters for the key - * @throws NoSuchAlgorithmException in case of missing algorithm support in the crypto backend - * @throws PGPException in case we cannot generate a binding signature for the subkey - * @throws IOException in case of an IO error - */ - SecretKeyRingEditorInterface addSubKey( - @Nonnull KeySpec keySpec, - @Nonnull Passphrase subKeyPassphrase, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException; - - /** - * Add a subkey to the key ring. - * The subkey will be generated from the provided {@link KeySpec}. - * - * @param keySpec key spec of the subkey - * @param subkeyPassphrase passphrase to encrypt the subkey - * @param subpacketsCallback callback to modify the subpackets of the subkey binding signature - * @param secretKeyRingProtector protector to unlock the primary key - * @return builder - * - * @throws InvalidAlgorithmParameterException in case the user wants to use invalid parameters for the key - * @throws NoSuchAlgorithmException in case of missing algorithm support in the crypto backend - * @throws PGPException in case we cannot generate a binding signature for the subkey - * @throws IOException in case of an IO error - */ - SecretKeyRingEditorInterface addSubKey( - @Nonnull KeySpec keySpec, - @Nonnull Passphrase subkeyPassphrase, - @Nullable SelfSignatureSubpackets.Callback subpacketsCallback, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException; - - /** - * Add a subkey to the key ring. - * - * @param subkey subkey key pair - * @param bindingSignatureCallback callback to modify the subpackets of the subkey binding signature - * @param subkeyProtector protector to unlock and encrypt the subkey - * @param primaryKeyProtector protector to unlock the primary key - * @param keyFlag first key flag for the subkey - * @param additionalKeyFlags optional additional key flags - * @return builder - * - * @throws PGPException in case we cannot generate a binding signature for the subkey - * @throws IOException in case of an IO error - */ - SecretKeyRingEditorInterface addSubKey( - @Nonnull PGPKeyPair subkey, - @Nullable SelfSignatureSubpackets.Callback bindingSignatureCallback, - @Nonnull SecretKeyRingProtector subkeyProtector, - @Nonnull SecretKeyRingProtector primaryKeyProtector, - @Nonnull KeyFlag keyFlag, - KeyFlag... additionalKeyFlags) - throws PGPException, IOException; - - /** - * Revoke the key ring. - * The revocation will be a hard revocation, rendering the whole key invalid for any past or future signatures. - * - * @param secretKeyRingProtector protector of the primary key - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature - */ - default SecretKeyRingEditorInterface revoke( - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return revoke(secretKeyRingProtector, (RevocationAttributes) null); - } - - /** - * Revoke the key ring using the provided revocation attributes. - * The attributes define, whether the revocation was a hard revocation or not. - * - * @param secretKeyRingProtector protector of the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature - */ - SecretKeyRingEditorInterface revoke( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke the key ring. - * You can use the {@link RevocationSignatureSubpackets.Callback} to modify the revocation signatures - * subpackets, e.g. in order to define whether this is a hard or soft revocation. - * - * @param secretKeyRingProtector protector to unlock the primary secret key - * @param subpacketsCallback callback to modify the revocations subpackets - * @return builder - * - * @throws PGPException in case we cannot generate a revocation signature - */ - SecretKeyRingEditorInterface revoke( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) throws PGPException; - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided fingerprint will be revoked. - * If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown. - * - * Note: This method will hard-revoke the provided subkey, meaning it cannot be re-certified at a later point. - * If you instead want to temporarily "deactivate" the subkey, provide a soft revocation reason, - * e.g. by calling {@link #revokeSubKey(OpenPgpFingerprint, SecretKeyRingProtector, RevocationAttributes)} - * and provide a suitable {@link RevocationAttributes} object. - * - * @param fingerprint fingerprint of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the secret key ring - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - default SecretKeyRingEditorInterface revokeSubKey( - @Nonnull OpenPgpFingerprint fingerprint, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return revokeSubKey(fingerprint, secretKeyRingProtector, null); - } - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided fingerprint will be revoked. - * If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown. - * - * @param fingerprint fingerprint of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - default SecretKeyRingEditorInterface revokeSubKey( - OpenPgpFingerprint fingerprint, - SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException { - return revokeSubKey(fingerprint.getKeyId(), - secretKeyRingProtector, - revocationAttributes); - } - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided key-id will be revoked. - * If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown. - * - * @param subKeyId id of the subkey - * @param secretKeyRingProtector protector to unlock the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - SecretKeyRingEditorInterface revokeSubKey( - long subKeyId, - SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided key-id will be revoked. - * If no suitable subkey is found, q {@link java.util.NoSuchElementException} will be thrown. - * - * Note: This method will hard-revoke the subkey, meaning it cannot be re-bound at a later point. - * If you intend to re-bind the subkey in order to make it usable again at a later point in time, - * consider using {@link #revokeSubKey(long, SecretKeyRingProtector, RevocationAttributes)} - * and provide a soft revocation reason. - * - * @param subKeyId id of the subkey - * @param secretKeyRingProtector protector to unlock the secret key ring - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - default SecretKeyRingEditorInterface revokeSubKey( - long subKeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - - return revokeSubKey( - subKeyId, - secretKeyRingProtector, - (RevocationSignatureSubpackets.Callback) null); - } - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided key-id will be revoked. - * If no suitable subkey is found, q {@link java.util.NoSuchElementException} will be thrown. - * - * The provided subpackets callback is used to modify the revocation signatures subpackets. - * - * @param keyID id of the subkey - * @param secretKeyRingProtector protector to unlock the secret key ring - * @param subpacketsCallback callback which can be used to modify the subpackets of the revocation - * signature - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - SecretKeyRingEditorInterface revokeSubKey( - long keyID, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException; - - /** - * Revoke the given userID. - * The revocation will be a hard revocation, rendering the user-id invalid for any past or future signatures. - * If you intend to re-certify the user-id at a later point in time, consider using - * {@link #revokeUserId(CharSequence, SecretKeyRingProtector, RevocationAttributes)} instead and provide - * a soft revocation reason. - * - * @param userId userId to revoke - * @param secretKeyRingProtector protector to unlock the primary key - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - default SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return revokeUserId(userId, secretKeyRingProtector, (RevocationAttributes) null); - } - - /** - * Revoke the given userID using the provided revocation attributes. - * - * @param userId userId to revoke - * @param secretKeyRingProtector protector to unlock the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke the provided user-id. - * Note: If you don't provide a {@link RevocationSignatureSubpackets.Callback} which - * sets a revocation reason ({@link RevocationAttributes}), the revocation might be considered hard. - * So if you intend to re-certify the user-id at a later point to make it valid again, - * make sure to set a soft revocation reason in the signatures hashed area using the subpacket callback. - * - * @param userId userid to be revoked - * @param secretKeyRingProtector protector to unlock the primary secret key - * @param subpacketCallback callback to modify the revocations subpackets - * @return builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketCallback) - throws PGPException; - - /** - * Revoke all user-ids that match the provided {@link SelectUserId} filter. - * The provided {@link RevocationAttributes} will be set as reason for revocation in each - * revocation signature. - * - * Note: If you intend to re-certify these user-ids at a later point, make sure to choose - * a soft revocation reason. See {@link RevocationAttributes.Reason} for more information. - * - * @param userIdSelector user-id selector - * @param secretKeyRingProtector protector to unlock the primary secret key - * @param revocationAttributes revocation attributes - * @return builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface revokeUserIds( - @Nonnull SelectUserId userIdSelector, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke all user-ids that match the provided {@link SelectUserId} filter. - * The provided {@link RevocationSignatureSubpackets.Callback} will be used to modify the - * revocation signatures subpackets. - * - * Note: If you intend to re-certify these user-ids at a later point, make sure to set - * a soft revocation reason in the revocation signatures hashed subpacket area using the callback. - * - * See {@link RevocationAttributes.Reason} for more information. - * - * @param userIdSelector user-id selector - * @param secretKeyRingProtector protector to unlock the primary secret key - * @param subpacketsCallback callback to modify the revocations subpackets - * @return builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface revokeUserIds( - @Nonnull SelectUserId userIdSelector, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException; - - /** - * Set the expiration date for the primary key of the key ring. - * If the key is supposed to never expire, then an expiration date of null is expected. - * - * @param expiration new expiration date or null - * @param secretKeyRingProtector to unlock the secret key - * @return the builder - * - * @throws PGPException in case we cannot generate a new self-signature with the changed expiration date - */ - SecretKeyRingEditorInterface setExpirationDate( - @Nullable Date expiration, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException; - - /** - * Create a minimal, self-authorizing revocation certificate, containing only the primary key - * and a revocation signature. - * This type of revocation certificates was introduced in OpenPGP v6. - * This method has no side effects on the original key and will leave it intact. - * - * @param secretKeyRingProtector protector to unlock the primary key. - * @param keyRevocationAttributes reason for the revocation (key revocation) - * @return minimal revocation certificate - * - * @throws PGPException in case we cannot generate a revocation signature - */ - PGPPublicKeyRing createMinimalRevocationCertificate(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes keyRevocationAttributes) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the whole key. - * The original key will not be modified by this method. - * - * @param secretKeyRingProtector protector to unlock the primary key. - * @param revocationAttributes reason for the revocation - * @return revocation certificate - * - * @throws PGPException in case we cannot generate a revocation certificate - */ - PGPSignature createRevocation( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the specified subkey. - * The original key will not be modified by this method. - * - * @param subkeyId id of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the primary key. - * @param revocationAttributes reason for the revocation - * @return revocation certificate - * - * @throws PGPException in case we cannot generate a revocation certificate - */ - PGPSignature createRevocation( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the specified subkey. - * The original key will not be modified by this method. - * - * @param subkeyId id of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the primary key. - * @param certificateSubpacketsCallback callback to modify the subpackets of the revocation certificate. - * @return revocation certificate - * - * @throws PGPException in case we cannot generate a revocation certificate - */ - PGPSignature createRevocation( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback certificateSubpacketsCallback) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the specified subkey. - * The original key will not be modified by this method. - * - * @param subkeyFingerprint fingerprint of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the primary key. - * @param revocationAttributes reason for the revocation - * @return revocation certificate - * - * @throws PGPException in case we cannot generate a revocation certificate - */ - default PGPSignature createRevocation( - OpenPgpFingerprint subkeyFingerprint, - SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - - return createRevocation( - subkeyFingerprint.getKeyId(), - secretKeyRingProtector, - revocationAttributes); - } - - /** - * Change the passphrase of the whole key ring. - * - * @param oldPassphrase old passphrase or null, if the key was unprotected - * @return next builder step - */ - default WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase( - @Nullable Passphrase oldPassphrase) { - return changePassphraseFromOldPassphrase(oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()); - } - - /** - * Change the passphrase of the whole key ring. - * - * @param oldPassphrase old passphrase or null, if the key was unprotected - * @param oldProtectionSettings custom settings for the old passphrase - * @return next builder step - */ - WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase( - @Nullable Passphrase oldPassphrase, - @Nonnull KeyRingProtectionSettings oldProtectionSettings); - - /** - * Change the passphrase of a single subkey in the key ring. - * - * Note: While it is a valid use-case to have different passphrases per subKey, - * this is one of the reasons why OpenPGP sucks in practice. - * - * @param keyId id of the subkey - * @param oldPassphrase old passphrase - * @return next builder step - */ - default WithKeyRingEncryptionSettings changeSubKeyPassphraseFromOldPassphrase( - @Nonnull Long keyId, - @Nullable Passphrase oldPassphrase) { - return changeSubKeyPassphraseFromOldPassphrase(keyId, oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()); - } - - WithKeyRingEncryptionSettings changeSubKeyPassphraseFromOldPassphrase( - @Nonnull Long keyId, - @Nullable Passphrase oldPassphrase, - @Nonnull KeyRingProtectionSettings oldProtectionSettings); - - interface WithKeyRingEncryptionSettings { - - /** - * Set secure default settings for the symmetric passphrase encryption. - * Note that this obviously has no effect if you decide to set {@link WithPassphrase#toNoPassphrase()}. - * - * @return next builder step - */ - WithPassphrase withSecureDefaultSettings(); - - /** - * Set custom settings for the symmetric passphrase encryption. - * - * @param settings custom settings - * @return next builder step - */ - WithPassphrase withCustomSettings(KeyRingProtectionSettings settings); - - } - - interface WithPassphrase { - - /** - * Set the passphrase. - * - * @param passphrase passphrase - * @return editor builder - * - * @throws PGPException in case the passphrase cannot be changed - */ - SecretKeyRingEditorInterface toNewPassphrase(Passphrase passphrase) - throws PGPException; - - /** - * Leave the key unprotected. - * - * @return editor builder - * - * @throws PGPException in case the passphrase cannot be changed - */ - SecretKeyRingEditorInterface toNoPassphrase() throws PGPException; - } - - /** - * Return the {@link PGPSecretKeyRing}. - * @return the key - */ - PGPSecretKeyRing done(); - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/package-info.java deleted file mode 100644 index 6b3eb3b3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes that deal with modifications made to {@link org.bouncycastle.openpgp.PGPSecretKeyRing PGPSecretKeyRings}. - */ -package org.pgpainless.key.modification.secretkeyring; diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt index 7ccddb42..afa38aa6 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt @@ -4,6 +4,7 @@ package org.bouncycastle.extensions +import openpgp.openPgpKeyId import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPOnePassSignature import org.bouncycastle.openpgp.PGPPublicKey @@ -46,6 +47,12 @@ fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean = fun PGPKeyRing.getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = this.getPublicKey(fingerprint.bytes) +fun PGPKeyRing.requirePublicKey(keyId: Long): PGPPublicKey = + getPublicKey(keyId) ?: throw NoSuchElementException("OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") + +fun PGPKeyRing.requirePublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey = + getPublicKey(fingerprint) ?: throw NoSuchElementException("OpenPGP key does not contain key with fingerprint $fingerprint.") + /** * Return the [PGPPublicKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. * If the [PGPSignature] does not carry an issuer-fingerprint subpacket, fall back to the issuer-keyID subpacket to diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt index 8ea1503c..76d3449c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -212,6 +212,48 @@ class KeyRingInfo( else userIdExpirationDate } + /** + * List of all subkeys that can be used to sign a message. + */ + val signingSubkeys: List = validSubkeys.filter { getKeyFlagsOf(it.keyID).contains(KeyFlag.SIGN_DATA) } + + /** + * Whether the key is usable for encryption. + */ + val isUsableForEncryption: Boolean = isUsableForEncryption(EncryptionPurpose.ANY) + + /** + * Whether the key is capable of signing messages. + * This field is also true, if the key contains a subkey that is capable of signing messages, but where the secret + * key is unavailable, e.g. because it was moved to a smart-card. + * + * To check for keys that are actually usable to sign messages, use [isUsableForSigning]. + */ + val isSigningCapable: Boolean = isKeyValidlyBound(keyId) && signingSubkeys.isNotEmpty() + + /** + * Whether the key is actually usable to sign messages. + */ + val isUsableForSigning: Boolean = isSigningCapable && signingSubkeys.any { isSecretKeyAvailable(it.keyID) } + + /** + * [HashAlgorithm] preferences of the primary user-ID or if absent, of the primary key. + */ + val preferredHashAlgorithms: Set + get() = primaryUserId?.let { getPreferredHashAlgorithms(it) } ?: getPreferredHashAlgorithms(keyId) + + /** + * [SymmetricKeyAlgorithm] preferences of the primary user-ID or if absent of the primary key. + */ + val preferredSymmetricKeyAlgorithms: Set + get() = primaryUserId?.let { getPreferredSymmetricKeyAlgorithms(it) } ?: getPreferredSymmetricKeyAlgorithms(keyId) + + /** + * [CompressionAlgorithm] preferences of the primary user-ID or if absent, the primary key. + */ + val preferredCompressionAlgorithms: Set + get() = primaryUserId?.let { getPreferredCompressionAlgorithms(it) } ?: getPreferredCompressionAlgorithms(keyId) + /** * Return the expiration date of the subkey with the provided fingerprint. * @@ -331,16 +373,6 @@ class KeyRingInfo( }.toList() } - /** - * List of all subkeys that can be used to sign a message. - */ - val signingSubkeys: List = validSubkeys.filter { getKeyFlagsOf(it.keyID).contains(KeyFlag.SIGN_DATA) } - - /** - * Whether the key is usable for encryption. - */ - val isUsableForEncryption: Boolean = isUsableForEncryption(EncryptionPurpose.ANY) - /** * Return, whether the key is usable for encryption, given the purpose. * @@ -350,20 +382,6 @@ class KeyRingInfo( return isKeyValidlyBound(keyId) && getEncryptionSubkeys(purpose).isNotEmpty() } - /** - * Whether the key is capable of signing messages. - * This field is also true, if the key contains a subkey that is capable of signing messages, but where the secret - * key is unavailable, e.g. because it was moved to a smart-card. - * - * To check for keys that are actually usable to sign messages, use [isUsableForSigning]. - */ - val isSigningCapable: Boolean = isKeyValidlyBound(keyId) && signingSubkeys.isNotEmpty() - - /** - * Whether the key is actually usable to sign messages. - */ - val isUsableForSigning: Boolean = isSigningCapable && signingSubkeys.any { isSecretKeyAvailable(it.keyID) } - /** * Return the primary user-ID, even if it is possibly expired. * @@ -621,7 +639,7 @@ class KeyRingInfo( return false } if (sig.hashedSubPackets.isPrimaryUserID) { - SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(sig, publicKey)?.let { expirationDate -> + getKeyExpirationTimeAsDate(sig, publicKey)?.let { expirationDate -> // key expired? if (expirationDate < referenceDate) return false } @@ -634,12 +652,6 @@ class KeyRingInfo( } ?: true // certification, but no revocation } ?: false // no certification - /** - * [HashAlgorithm] preferences of the primary user-ID or if absent, of the primary key. - */ - val preferredHashAlgorithms: Set - get() = primaryUserId?.let { getPreferredHashAlgorithms(it) } ?: getPreferredHashAlgorithms(keyId) - /** * [HashAlgorithm] preferences of the given user-ID. */ @@ -654,12 +666,6 @@ class KeyRingInfo( return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredHashAlgorithms } - /** - * [SymmetricKeyAlgorithm] preferences of the primary user-ID or if absent of the primary key. - */ - val preferredSymmetricKeyAlgorithms: Set - get() = primaryUserId?.let { getPreferredSymmetricKeyAlgorithms(it) } ?: getPreferredSymmetricKeyAlgorithms(keyId) - /** * [SymmetricKeyAlgorithm] preferences of the given user-ID. */ @@ -674,12 +680,6 @@ class KeyRingInfo( return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredSymmetricKeyAlgorithms } - /** - * [CompressionAlgorithm] preferences of the primary user-ID or if absent, the primary key. - */ - val preferredCompressionAlgorithms: Set - get() = primaryUserId?.let { getPreferredCompressionAlgorithms(it) } ?: getPreferredCompressionAlgorithms(keyId) - /** * [CompressionAlgorithm] preferences of the given user-ID. */ diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt new file mode 100644 index 00000000..2fde469c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -0,0 +1,493 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification.secretkeyring + +import org.bouncycastle.bcpg.sig.KeyExpirationTime +import org.bouncycastle.extensions.getKeyExpirationDate +import org.bouncycastle.extensions.publicKeyAlgorithm +import org.bouncycastle.extensions.requirePublicKey +import org.bouncycastle.openpgp.* +import org.pgpainless.PGPainless +import org.pgpainless.PGPainless.Companion.inspectKeyRing +import org.pgpainless.algorithm.AlgorithmSuite +import org.pgpainless.algorithm.Feature +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.generation.KeyRingBuilder +import org.pgpainless.key.generation.KeySpec +import org.pgpainless.key.protection.* +import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.key.util.KeyRingUtils.Companion.changePassphrase +import org.pgpainless.key.util.KeyRingUtils.Companion.injectCertification +import org.pgpainless.key.util.RevocationAttributes +import org.pgpainless.signature.builder.* +import org.pgpainless.signature.subpackets.* +import org.pgpainless.util.Passphrase +import org.pgpainless.util.selection.userid.SelectUserId +import java.util.* +import javax.annotation.Nonnull + +class SecretKeyRingEditor( + var secretKeyRing: PGPSecretKeyRing, + override val referenceTime: Date = Date() +) : SecretKeyRingEditorInterface { + + override fun addUserId(userId: CharSequence, + callback: SelfSignatureSubpackets.Callback?, + protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + val sanitizedUserId = sanitizeUserId(userId).toString() + val primaryKey = secretKeyRing.secretKey + + val info = inspectKeyRing(secretKeyRing, referenceTime) + require(!info.isHardRevoked(userId)) { + "User-ID $userId is hard revoked and cannot be re-certified." + } + + val (hashAlgorithmPreferences, symmetricKeyAlgorithmPreferences, compressionAlgorithmPreferences) = try { + Triple(info.preferredHashAlgorithms, info.preferredSymmetricKeyAlgorithms, info.preferredCompressionAlgorithms) + } catch (e : IllegalStateException) { // missing user-id sig + val algorithmSuite = AlgorithmSuite.defaultAlgorithmSuite + Triple(algorithmSuite.hashAlgorithms, algorithmSuite.symmetricKeyAlgorithms, algorithmSuite.compressionAlgorithms) + } + + val builder = SelfSignatureBuilder(primaryKey, protector).apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + setSignatureType(SignatureType.POSITIVE_CERTIFICATION) + } + builder.hashedSubpackets.apply { + setKeyFlags(info.getKeyFlagsOf(primaryKey.keyID)) + setPreferredHashAlgorithms(hashAlgorithmPreferences) + setPreferredSymmetricKeyAlgorithms(symmetricKeyAlgorithmPreferences) + setPreferredCompressionAlgorithms(compressionAlgorithmPreferences) + setFeatures(Feature.MODIFICATION_DETECTION) + } + builder.applyCallback(callback) + secretKeyRing = injectCertification(secretKeyRing, sanitizedUserId, builder.build(primaryKey.publicKey, sanitizedUserId)) + return this + } + + override fun addPrimaryUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + val uid = sanitizeUserId(userId) + val primaryKey = secretKeyRing.publicKey + var info = inspectKeyRing(secretKeyRing, referenceTime) + val primaryUserId = info.primaryUserId + val signature = if (primaryUserId == null) info.latestDirectKeySelfSignature else info.getLatestUserIdCertification(primaryUserId) + val previousKeyExpiration = signature?.getKeyExpirationDate(primaryKey.creationTime) + + // Add new primary user-id signature + addUserId(uid, object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + hashedSubpackets.apply { + setPrimaryUserId() + if (previousKeyExpiration != null) setKeyExpirationTime(primaryKey, previousKeyExpiration) + else setKeyExpirationTime(null) + } + } + }, protector) + + // unmark previous primary user-ids to be non-primary + info = inspectKeyRing(secretKeyRing, referenceTime) + info.validAndExpiredUserIds.filterNot { it == uid }.forEach { otherUserId -> + if (info.getLatestUserIdCertification(otherUserId)!!.hashedSubPackets.isPrimaryUserID) { + // We need to unmark this user-id as primary + addUserId(otherUserId, object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + hashedSubpackets.apply { + setPrimaryUserId(null) + setKeyExpirationTime(null) // non-primary + } + } + }, protector) + } + } + return this + } + + override fun removeUserId(selector: SelectUserId, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + return revokeUserIds(selector, protector, RevocationAttributes.createCertificateRevocation() + .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) + .withoutDescription()) + } + + override fun removeUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + return removeUserId(SelectUserId.exactMatch(userId), protector) + } + + override fun replaceUserId(oldUserId: CharSequence, newUserId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + val oldUID = sanitizeUserId(oldUserId) + val newUID = sanitizeUserId(newUserId) + require(oldUID.isNotBlank()) { "Old user-ID cannot be empty." } + require(newUID.isNotBlank()) { "New user-ID cannot be empty." } + + val info = inspectKeyRing(secretKeyRing, referenceTime) + if (!info.isUserIdValid(oldUID)) { + throw NoSuchElementException("Key does not carry user-ID '$oldUID', or it is not valid.") + } + + val oldCertification = info.getLatestUserIdCertification(oldUID) + ?: throw AssertionError("Certification for old user-ID MUST NOT be null.") + + addUserId(newUID, object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets?) { + SignatureSubpacketsHelper.applyFrom(oldCertification.hashedSubPackets, hashedSubpackets as SignatureSubpackets) + if (oldUID == info.primaryUserId && !oldCertification.hashedSubPackets.isPrimaryUserID) { + hashedSubpackets.setPrimaryUserId() + } + } + + override fun modifyUnhashedSubpackets(unhashedSubpackets: SelfSignatureSubpackets?) { + SignatureSubpacketsHelper.applyFrom(oldCertification.unhashedSubPackets, unhashedSubpackets as SignatureSubpackets) + } + }, protector) + + return revokeUserId(oldUID, protector) + } + + override fun addSubKey(keySpec: KeySpec, + subkeyPassphrase: Passphrase, + protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + val callback = object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets?) { + SignatureSubpacketsHelper.applyFrom(keySpec.subpackets, hashedSubpackets as SignatureSubpackets) + } + } + return addSubKey(keySpec, subkeyPassphrase, callback, protector) + } + + override fun addSubKey(keySpec: KeySpec, + subkeyPassphrase: Passphrase, + callback: SelfSignatureSubpackets.Callback?, + protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + val keyPair = KeyRingBuilder.generateKeyPair(keySpec) + val subkeyProtector = PasswordBasedSecretKeyRingProtector.forKeyId(keyPair.keyID, subkeyPassphrase) + val keyFlags = KeyFlag.fromBitmask(keySpec.subpackets.keyFlags).toMutableList() + return addSubKey(keyPair, callback, subkeyProtector, protector, keyFlags.removeFirst(), *keyFlags.toTypedArray()) + } + + override fun addSubKey(subkey: PGPKeyPair, + callback: SelfSignatureSubpackets.Callback?, + subkeyProtector: SecretKeyRingProtector, + primaryKeyProtector: SecretKeyRingProtector, + keyFlag: KeyFlag, + vararg keyFlags: KeyFlag): SecretKeyRingEditorInterface { + val flags = listOf(keyFlag).plus(keyFlags) + val subkeyAlgorithm = subkey.publicKey.publicKeyAlgorithm + SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm) + + val bitStrength = subkey.publicKey.bitStrength + require(PGPainless.getPolicy().publicKeyAlgorithmPolicy.isAcceptable(subkeyAlgorithm, bitStrength)) { + "Public key algorithm policy violation: $subkeyAlgorithm with bit strength $bitStrength is not acceptable." + } + + val primaryKey = secretKeyRing.secretKey + val info = inspectKeyRing(secretKeyRing, referenceTime) + val hashAlgorithm = HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) + .negotiateHashAlgorithm(info.preferredHashAlgorithms) + + var secretSubkey = PGPSecretKey(subkey.privateKey, subkey.publicKey, + ImplementationFactory.getInstance().v4FingerprintCalculator, + false, subkeyProtector.getEncryptor(subkey.keyID)) + val skBindingBuilder = SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector, hashAlgorithm) + skBindingBuilder.apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + hashedSubpackets.setKeyFlags(flags) + if (subkeyAlgorithm.isSigningCapable()) { + val pkBindingBuilder = PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm) + pkBindingBuilder.hashedSubpackets.setSignatureCreationTime(referenceTime) + hashedSubpackets.addEmbeddedSignature(pkBindingBuilder.build(primaryKey.publicKey)) + } + applyCallback(callback) + } + secretSubkey = KeyRingUtils.secretKeyPlusSignature(secretSubkey, skBindingBuilder.build(secretSubkey.publicKey)) + secretKeyRing = KeyRingUtils.keysPlusSecretKey(secretKeyRing, secretSubkey) + return this + } + + override fun revoke(protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface { + return revoke(protector, callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun revoke(protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + return revokeSubKey(secretKeyRing.secretKey.keyID, protector, callback) + } + + override fun revokeSubKey(subkeyId: Long, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface { + return revokeSubKey(subkeyId, protector, callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun revokeSubKey(subkeyId: Long, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + val revokeeSubKey = secretKeyRing.requirePublicKey(subkeyId) + val subkeyRevocation = generateRevocation(protector, revokeeSubKey, callback) + secretKeyRing = injectCertification(secretKeyRing, revokeeSubKey, subkeyRevocation) + return this + } + + override fun revokeUserId(userId: CharSequence, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface { + if (revocationAttributes != null) { + require(revocationAttributes.reason == RevocationAttributes.Reason.NO_REASON || + revocationAttributes.reason == RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) { + "Revocation reason must either be NO_REASON or USER_ID_NO_LONGER_VALID" + } + } + + return revokeUserId(userId, protector, object : RevocationSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: RevocationSignatureSubpackets) { + if (revocationAttributes != null) { + hashedSubpackets.setRevocationReason(false, revocationAttributes) + } + } + }) + } + + override fun revokeUserId(userId: CharSequence, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + return revokeUserIds(SelectUserId.exactMatch(sanitizeUserId(userId)), protector, callback) + } + + override fun revokeUserIds(selector: SelectUserId, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface { + return revokeUserIds(selector, protector, object : RevocationSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: RevocationSignatureSubpackets) { + if (revocationAttributes != null) { + hashedSubpackets.setRevocationReason(revocationAttributes) + } + } + }) + } + + override fun revokeUserIds(selector: SelectUserId, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + selector.selectUserIds(secretKeyRing).also { + if (it.isEmpty()) throw NoSuchElementException("No matching user-ids found on the key.") + }.forEach { userId -> doRevokeUserId(userId, protector, callback) } + return this + } + + override fun setExpirationDate(expiration: Date?, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + require(secretKeyRing.secretKey.isMasterKey) { + "OpenPGP key does not appear to contain a primary secret key." + } + + val prevDirectKeySig = getPreviousDirectKeySignature() + // reissue direct key sig + if (prevDirectKeySig != null) { + secretKeyRing = injectCertification(secretKeyRing, secretKeyRing.publicKey, + reissueDirectKeySignature(expiration, protector, prevDirectKeySig)) + } + + val primaryUserId = inspectKeyRing(secretKeyRing, referenceTime).getPossiblyExpiredPrimaryUserId() + if (primaryUserId != null) { + val prevUserIdSig = getPreviousUserIdSignatures(primaryUserId) + val userIdSig = reissuePrimaryUserIdSig(expiration, protector, primaryUserId, prevUserIdSig!!) + secretKeyRing = injectCertification(secretKeyRing, primaryUserId, userIdSig) + } + + val info = inspectKeyRing(secretKeyRing, referenceTime) + for (userId in info.validUserIds) { + if (userId == primaryUserId) { + continue + } + + val prevUserIdSig = info.getLatestUserIdCertification(userId) ?: throw AssertionError("A valid user-id shall never have no user-id signature.") + if (prevUserIdSig.hashedSubPackets.isPrimaryUserID) { + secretKeyRing = injectCertification(secretKeyRing, primaryUserId!!, + reissueNonPrimaryUserId(protector, userId, prevUserIdSig)) + } + } + + return this + } + + override fun createMinimalRevocationCertificate(protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): PGPPublicKeyRing { + // Check reason + if (revocationAttributes != null) { + require(RevocationAttributes.Reason.isKeyRevocation(revocationAttributes.reason)) { + "Revocation reason MUST be applicable to a key revocation." + } + } + + val revocation = createRevocation(protector, revocationAttributes) + var primaryKey = secretKeyRing.secretKey.publicKey + primaryKey = KeyRingUtils.getStrippedDownPublicKey(primaryKey) + primaryKey = PGPPublicKey.addCertification(primaryKey, revocation) + return PGPPublicKeyRing(listOf(primaryKey)) + } + + override fun createRevocation(protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): PGPSignature { + return generateRevocation(protector, secretKeyRing.publicKey, callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun createRevocation(subkeyId: Long, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): PGPSignature { + return generateRevocation(protector, secretKeyRing.requirePublicKey(subkeyId), callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun createRevocation(subkeyId: Long, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): PGPSignature { + return generateRevocation(protector, secretKeyRing.requirePublicKey(subkeyId), callback) + } + + override fun createRevocation(subkeyFingerprint: OpenPgpFingerprint, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): PGPSignature { + return generateRevocation(protector, secretKeyRing.requirePublicKey(subkeyFingerprint), callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun changePassphraseFromOldPassphrase(oldPassphrase: Passphrase, oldProtectionSettings: KeyRingProtectionSettings): SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { + return WithKeyRingEncryptionSettingsImpl(this, null, + PasswordBasedSecretKeyRingProtector(oldProtectionSettings, SolitaryPassphraseProvider(oldPassphrase))) + } + + override fun changeSubKeyPassphraseFromOldPassphrase(keyId: Long, oldPassphrase: Passphrase, oldProtectionSettings: KeyRingProtectionSettings): SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { + return WithKeyRingEncryptionSettingsImpl(this, keyId, + CachingSecretKeyRingProtector(mapOf(keyId to oldPassphrase), oldProtectionSettings, null)) + } + + override fun done(): PGPSecretKeyRing { + return secretKeyRing + } + + private fun sanitizeUserId(userId: CharSequence): CharSequence = + // TODO: Further research how to sanitize user IDs. + // eg. what about newlines? + userId.toString().trim() + + private fun callbackFromRevocationAttributes(attributes: RevocationAttributes?) = + object : RevocationSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: RevocationSignatureSubpackets) { + if (attributes != null) { + hashedSubpackets.setRevocationReason(attributes) + } + } + } + + private fun generateRevocation(protector: SecretKeyRingProtector, + revokeeSubkey: PGPPublicKey, + callback: RevocationSignatureSubpackets.Callback?): PGPSignature { + val primaryKey = secretKeyRing.secretKey + val signatureType = + if (revokeeSubkey.isMasterKey) SignatureType.KEY_REVOCATION + else SignatureType.SUBKEY_REVOCATION + + return RevocationSignatureBuilder(signatureType, primaryKey, protector) + .apply { applyCallback(callback) } + .build(revokeeSubkey) + } + + private fun doRevokeUserId(userId: CharSequence, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + RevocationSignatureBuilder(SignatureType.CERTIFICATION_REVOCATION, secretKeyRing.secretKey, protector).apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + applyCallback(callback) + }.let { + secretKeyRing = injectCertification(secretKeyRing, userId, it.build(userId.toString())) + } + return this + } + + + private fun getPreviousDirectKeySignature(): PGPSignature? { + val info = inspectKeyRing(secretKeyRing, referenceTime) + return info.latestDirectKeySelfSignature + } + + private fun getPreviousUserIdSignatures(userId: String): PGPSignature? { + val info = inspectKeyRing(secretKeyRing, referenceTime) + return info.getLatestUserIdCertification(userId) + } + + @Throws(PGPException::class) + private fun reissueNonPrimaryUserId( + secretKeyRingProtector: SecretKeyRingProtector, + userId: String, + prevUserIdSig: PGPSignature): PGPSignature { + val builder = SelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevUserIdSig) + builder.hashedSubpackets.setSignatureCreationTime(referenceTime) + builder.applyCallback(object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + // unmark as primary + hashedSubpackets.setPrimaryUserId(null) + } + }) + return builder.build(secretKeyRing.publicKey, userId) + } + + @Throws(PGPException::class) + private fun reissuePrimaryUserIdSig( + expiration: Date?, + @Nonnull secretKeyRingProtector: SecretKeyRingProtector, + @Nonnull primaryUserId: String, + @Nonnull prevUserIdSig: PGPSignature): PGPSignature { + return SelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevUserIdSig) + .apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + applyCallback(object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + if (expiration != null) { + hashedSubpackets.setKeyExpirationTime(true, secretKeyRing.publicKey.creationTime, expiration) + } else { + hashedSubpackets.setKeyExpirationTime(KeyExpirationTime(true, 0)) + } + hashedSubpackets.setPrimaryUserId() + } + }) + }.build(secretKeyRing.publicKey, primaryUserId) + } + + @Throws(PGPException::class) + private fun reissueDirectKeySignature( + expiration: Date?, + secretKeyRingProtector: SecretKeyRingProtector, + prevDirectKeySig: PGPSignature): PGPSignature { + return DirectKeySelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevDirectKeySig) + .apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + applyCallback(object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + if (expiration != null) { + hashedSubpackets.setKeyExpirationTime(secretKeyRing.publicKey.creationTime, expiration) + } else { + hashedSubpackets.setKeyExpirationTime(null) + } + } + }) + }.build(secretKeyRing.publicKey) + } + + private class WithKeyRingEncryptionSettingsImpl( + private val editor: SecretKeyRingEditor, + private val keyId: Long?, + private val oldProtector: SecretKeyRingProtector) : SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { + + override fun withSecureDefaultSettings(): SecretKeyRingEditorInterface.WithPassphrase { + return withCustomSettings(KeyRingProtectionSettings.secureDefaultSettings()) + } + + override fun withCustomSettings(settings: KeyRingProtectionSettings): SecretKeyRingEditorInterface.WithPassphrase { + return WithPassphraseImpl(editor, keyId, oldProtector, settings) + } + } + + private class WithPassphraseImpl( + private val editor: SecretKeyRingEditor, + private val keyId: Long?, + private val oldProtector: SecretKeyRingProtector, + private val newProtectionSettings: KeyRingProtectionSettings + ) : SecretKeyRingEditorInterface.WithPassphrase { + + override fun toNewPassphrase(passphrase: Passphrase): SecretKeyRingEditorInterface { + val protector = PasswordBasedSecretKeyRingProtector(newProtectionSettings, SolitaryPassphraseProvider(passphrase)) + val secretKeys = changePassphrase(keyId, editor.secretKeyRing, oldProtector, protector) + editor.secretKeyRing = secretKeys + return editor + } + + override fun toNoPassphrase(): SecretKeyRingEditorInterface { + val protector = UnprotectedKeysProtector() + val secretKeys = changePassphrase(keyId, editor.secretKeyRing, oldProtector, protector) + editor.secretKeyRing = secretKeys + return editor + } + } + +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt new file mode 100644 index 00000000..45a09fb2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt @@ -0,0 +1,560 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification.secretkeyring + +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPKeyPair +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.generation.KeySpec +import org.pgpainless.key.protection.KeyRingProtectionSettings +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.util.RevocationAttributes +import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets +import org.pgpainless.util.Passphrase +import org.pgpainless.util.selection.userid.SelectUserId +import java.io.IOException +import java.security.InvalidAlgorithmParameterException +import java.security.NoSuchAlgorithmException +import java.util.* + +interface SecretKeyRingEditorInterface { + + /** + * Editors reference time. + * This time is used as creation date for new signatures, or as reference when evaluating expiration of + * existing signatures. + */ + val referenceTime: Date + + /** + * Add a user-id to the key ring. + * + * @param userId user-id + * @param protector protector to unlock the secret key + * @return the builder + * + * @throws PGPException in case we cannot generate a signature for the user-id + */ + @Throws(PGPException::class) + fun addUserId(userId: CharSequence, protector: SecretKeyRingProtector) = addUserId(userId, null, protector) + + /** + * Add a user-id to the key ring. + * + * @param userId user-id + * @param callback callback to modify the self-signature subpackets + * @param protector protector to unlock the secret key + * @return the builder + * + * @throws PGPException in case we cannot generate a signature for the user-id + */ + @Throws(PGPException::class) + fun addUserId(userId: CharSequence, callback: SelfSignatureSubpackets.Callback? = null, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Add a user-id to the key ring and mark it as primary. + * If the user-id is already present, a new certification signature will be created. + * + * @param userId user id + * @param protector protector to unlock the secret key + * @return the builder + * + * @throws PGPException in case we cannot generate a signature for the user-id + */ + @Throws(PGPException::class) + fun addPrimaryUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Convenience method to revoke selected user-ids using soft revocation signatures. + * The revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID], so that the user-id + * can be re-certified at a later point. + * + * @param selector selector to select user-ids + * @param protector protector to unlock the primary key + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun removeUserId(selector: SelectUserId, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Convenience method to revoke a single user-id using a soft revocation signature. + * The revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID] so that the user-id + * can be re-certified at a later point. + * + * @param userId user-id to revoke + * @param protector protector to unlock the primary key + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun removeUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Replace a user-id on the key with a new one. + * The old user-id gets soft revoked and the new user-id gets bound with the same signature subpackets as the + * old one, with one exception: + * If the old user-id was implicitly primary (did not carry a [org.bouncycastle.bcpg.sig.PrimaryUserID] packet, + * but effectively was primary), then the new user-id will be explicitly marked as primary. + * + * @param oldUserId old user-id + * @param newUserId new user-id + * @param protector protector to unlock the secret key + * @return the builder + * @throws PGPException in case we cannot generate a revocation and certification signature + * @throws java.util.NoSuchElementException if the old user-id was not found on the key; or if the oldUserId + * was already invalid + */ + @Throws(PGPException::class) + fun replaceUserId(oldUserId: CharSequence, newUserId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Add a subkey to the key ring. + * The subkey will be generated from the provided [KeySpec]. + * + * @param keySpec key specification + * @param subkeyPassphrase passphrase to encrypt the sub key + * @param callback callback to modify the subpackets of the subkey binding signature + * @param protector protector to unlock the secret key of the key ring + * @return the builder + * + * @throws InvalidAlgorithmParameterException in case the user wants to use invalid parameters for the key + * @throws NoSuchAlgorithmException in case of missing algorithm support in the crypto backend + * @throws PGPException in case we cannot generate a binding signature for the subkey + * @throws IOException in case of an IO error + */ + @Throws(PGPException::class, IOException::class, InvalidAlgorithmParameterException::class, NoSuchAlgorithmException::class) + fun addSubKey(keySpec: KeySpec, subkeyPassphrase: Passphrase, callback: SelfSignatureSubpackets.Callback? = null, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Add a subkey to the key ring. + * + * @param subkey subkey key pair + * @param callback callback to modify the subpackets of the subkey binding signature + * @param subkeyProtector protector to unlock and encrypt the subkey + * @param primaryKeyProtector protector to unlock the primary key + * @param keyFlag first mandatory key flag for the subkey + * @param keyFlags optional additional key flags + * @return builder + * + * @throws PGPException in case we cannot generate a binding signature for the subkey + * @throws IOException in case of an IO error + */ + @Throws(PGPException::class, IOException::class) + fun addSubKey(subkey: PGPKeyPair, + callback: SelfSignatureSubpackets.Callback?, + subkeyProtector: SecretKeyRingProtector, + primaryKeyProtector: SecretKeyRingProtector, + keyFlag: KeyFlag, + vararg keyFlags: KeyFlag): SecretKeyRingEditorInterface + + /** + * Revoke the key ring using a hard revocation. + * + * @param protector protector of the primary key + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature + */ + @Throws(PGPException::class) + fun revoke(protector: SecretKeyRingProtector) = revoke(protector, null as RevocationAttributes?) + + /** + * Revoke the key ring using the provided revocation attributes. + * The attributes define, whether the revocation was a hard revocation or not. + * + * @param protector protector of the primary key + * @param revocationAttributes reason for the revocation + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature + */ + @Throws(PGPException::class) + fun revoke(protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? = null): SecretKeyRingEditorInterface + + /** + * Revoke the key ring. + * You can use the [RevocationSignatureSubpackets.Callback] to modify the revocation signatures + * subpackets, e.g. in order to define whether this is a hard or soft revocation. + * + * @param protector protector to unlock the primary secret key + * @param callback callback to modify the revocations subpackets + * @return builder + * + * @throws PGPException in case we cannot generate a revocation signature + */ + @Throws(PGPException::class) + fun revoke(protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface + + /** + * Revoke the subkey binding signature of a subkey. + * The subkey with the provided fingerprint will be revoked. + * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * @param fingerprint fingerprint of the subkey to be revoked + * @param protector protector to unlock the primary key + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey(fingerprint: OpenPgpFingerprint, + protector: SecretKeyRingProtector) = revokeSubKey(fingerprint, protector, null) + + /** + * Revoke the subkey binding signature of a subkey. + * The subkey with the provided fingerprint will be revoked. + * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * @param fingerprint fingerprint of the subkey to be revoked + * @param protector protector to unlock the primary key + * @param revocationAttributes reason for the revocation + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey(fingerprint: OpenPgpFingerprint, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null): SecretKeyRingEditorInterface = + revokeSubKey(fingerprint.keyId, protector, revocationAttributes) + + /** + * Revoke the subkey binding signature of a subkey. + * The subkey with the provided key-id will be revoked. + * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * @param subkeyId id of the subkey + * @param protector protector to unlock the primary key + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey(subkeyId: Long, protector: SecretKeyRingProtector) = + revokeSubKey(subkeyId, protector, null as RevocationAttributes?) + + /** + * Revoke the subkey binding signature of a subkey. + * The subkey with the provided key-id will be revoked. + * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * @param subkeyId id of the subkey + * @param protector protector to unlock the primary key + * @param revocationAttributes reason for the revocation + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey(subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null): SecretKeyRingEditorInterface + + /** + * Revoke the subkey binding signature of a subkey. + * The subkey with the provided key-id will be revoked. + * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * The provided subpackets callback is used to modify the revocation signatures subpackets. + * + * @param subkeyId id of the subkey + * @param protector protector to unlock the secret key ring + * @param callback callback which can be used to modify the subpackets of the revocation + * signature + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey(subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface + + /** + * Hard-revoke the given userID. + * + * @param userId userId to revoke + * @param protector protector to unlock the primary key + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserId(userId: CharSequence, protector: SecretKeyRingProtector) = revokeUserId(userId, protector, null as RevocationAttributes?) + + /** + * Revoke the given userID using the provided revocation attributes. + * + * @param userId userId to revoke + * @param protector protector to unlock the primary key + * @param revocationAttributes reason for the revocation + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserId(userId: CharSequence, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null): SecretKeyRingEditorInterface + + /** + * Revoke the provided user-id. + * Note: If you don't provide a [RevocationSignatureSubpackets.Callback] which + * sets a revocation reason ([RevocationAttributes]), the revocation will be considered hard. + * So if you intend to re-certify the user-id at a later point to make it valid again, + * make sure to set a soft revocation reason in the signatures hashed area using the subpacket callback. + * + * @param userId userid to be revoked + * @param protector protector to unlock the primary secret key + * @param callback callback to modify the revocations subpackets + * @return builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserId(userId: CharSequence, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface + + /** + * Revoke all user-ids that match the provided [SelectUserId] filter. + * The provided [RevocationAttributes] will be set as reason for revocation in each + * revocation signature. + * + * Note: If you intend to re-certify these user-ids at a later point, make sure to choose + * a soft revocation reason. See [RevocationAttributes.Reason] for more information. + * + * @param selector user-id selector + * @param protector protector to unlock the primary secret key + * @param revocationAttributes revocation attributes + * @return builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserIds(selector: SelectUserId, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface + + /** + * Revoke all user-ids that match the provided [SelectUserId] filter. + * The provided [RevocationSignatureSubpackets.Callback] will be used to modify the + * revocation signatures subpackets. + * + * Note: If you intend to re-certify these user-ids at a later point, make sure to set + * a soft revocation reason in the revocation signatures hashed subpacket area using the callback. + * + * See [RevocationAttributes.Reason] for more information. + * + * @param selector user-id selector + * @param protector protector to unlock the primary secret key + * @param callback callback to modify the revocations subpackets + * @return builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserIds(selector: SelectUserId, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface + + /** + * Set the expiration date for the primary key of the key ring. + * If the key is supposed to never expire, then an expiration date of null is expected. + * + * @param expiration new expiration date or null + * @param protector to unlock the secret key + * @return the builder + * + * @throws PGPException in case we cannot generate a new self-signature with the changed expiration date + */ + @Throws(PGPException::class) + fun setExpirationDate(expiration: Date?, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Create a minimal, self-authorizing revocation certificate, containing only the primary key + * and a revocation signature. + * This type of revocation certificates was introduced in OpenPGP v6. + * This method has no side effects on the original key and will leave it intact. + * + * @param protector protector to unlock the primary key. + * @param revocationAttributes reason for the revocation (key revocation) + * @return minimal revocation certificate + * + * @throws PGPException in case we cannot generate a revocation signature + */ + @Throws(PGPException::class) + fun createMinimalRevocationCertificate(protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?): PGPPublicKeyRing + + /** + * Create a detached revocation certificate, which can be used to revoke the whole key. + * The original key will not be modified by this method. + * + * @param protector protector to unlock the primary key. + * @param revocationAttributes reason for the revocation + * @return revocation certificate + * + * @throws PGPException in case we cannot generate a revocation certificate + */ + @Throws(PGPException::class) + fun createRevocation(protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?): PGPSignature + + /** + * Create a detached revocation certificate, which can be used to revoke the specified subkey. + * The original key will not be modified by this method. + * + * @param subkeyId id of the subkey to be revoked + * @param protector protector to unlock the primary key. + * @param revocationAttributes reason for the revocation + * @return revocation certificate + * + * @throws PGPException in case we cannot generate a revocation certificate + */ + @Throws(PGPException::class) + fun createRevocation(subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?): PGPSignature + + /** + * Create a detached revocation certificate, which can be used to revoke the specified subkey. + * The original key will not be modified by this method. + * + * @param subkeyId id of the subkey to be revoked + * @param protector protector to unlock the primary key. + * @param callback callback to modify the subpackets of the revocation certificate. + * @return revocation certificate + * + * @throws PGPException in case we cannot generate a revocation certificate + */ + @Throws(PGPException::class) + fun createRevocation(subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?): PGPSignature + + /** + * Create a detached revocation certificate, which can be used to revoke the specified subkey. + * The original key will not be modified by this method. + * + * @param subkeyFingerprint fingerprint of the subkey to be revoked + * @param protector protector to unlock the primary key. + * @param revocationAttributes reason for the revocation + * @return revocation certificate + * + * @throws PGPException in case we cannot generate a revocation certificate + */ + @Throws(PGPException::class) + fun createRevocation(subkeyFingerprint: OpenPgpFingerprint, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?): PGPSignature + + /** + * Change the passphrase of the whole key ring. + * + * @param oldPassphrase old passphrase (empty, if the key was unprotected) + * @return next builder step + */ + fun changePassphraseFromOldPassphrase( + oldPassphrase: Passphrase) = changePassphraseFromOldPassphrase(oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()) + + /** + * Change the passphrase of the whole key ring. + * + * @param oldPassphrase old passphrase (empty, if the key was unprotected) + * @param oldProtectionSettings custom settings for the old passphrase + * @return next builder step + */ + fun changePassphraseFromOldPassphrase( + oldPassphrase: Passphrase, + oldProtectionSettings: KeyRingProtectionSettings = KeyRingProtectionSettings.secureDefaultSettings()): WithKeyRingEncryptionSettings + + /** + * Change the passphrase of a single subkey in the key ring. + * + * Note: While it is a valid use-case to have different passphrases per subKey, + * this is one of the reasons why OpenPGP sucks in practice. + * + * @param keyId id of the subkey + * @param oldPassphrase old passphrase (empty if the key was unprotected) + * @return next builder step + */ + fun changeSubKeyPassphraseFromOldPassphrase(keyId: Long, oldPassphrase: Passphrase) = + changeSubKeyPassphraseFromOldPassphrase(keyId, oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()) + + /** + * Change the passphrase of a single subkey in the key ring. + * + * Note: While it is a valid use-case to have different passphrases per subKey, + * this is one of the reasons why OpenPGP sucks in practice. + * + * @param keyId id of the subkey + * @param oldPassphrase old passphrase (empty if the key was unprotected) + * @param oldProtectionSettings custom settings for the old passphrase + * @return next builder step + */ + fun changeSubKeyPassphraseFromOldPassphrase( + keyId: Long, + oldPassphrase: Passphrase, + oldProtectionSettings: KeyRingProtectionSettings): WithKeyRingEncryptionSettings + + interface WithKeyRingEncryptionSettings { + + /** + * Set secure default settings for the symmetric passphrase encryption. + * Note that this obviously has no effect if you decide to set [WithPassphrase.toNoPassphrase]. + * + * @return next builder step + */ + fun withSecureDefaultSettings(): WithPassphrase + + /** + * Set custom settings for the symmetric passphrase encryption. + * + * @param settings custom settings + * @return next builder step + */ + fun withCustomSettings(settings: KeyRingProtectionSettings): WithPassphrase + } + + interface WithPassphrase { + + /** + * Set the passphrase. + * + * @param passphrase passphrase + * @return editor builder + * + * @throws PGPException in case the passphrase cannot be changed + */ + @Throws(PGPException::class) + fun toNewPassphrase(passphrase: Passphrase): SecretKeyRingEditorInterface + + /** + * Leave the key unprotected. + * + * @return editor builder + * + * @throws PGPException in case the passphrase cannot be changed + */ + @Throws(PGPException::class) + fun toNoPassphrase(): SecretKeyRingEditorInterface + } + + /** + * Return the [PGPSecretKeyRing]. + * @return the key + */ + fun done(): PGPSecretKeyRing + fun addSubKey(keySpec: KeySpec, subkeyPassphrase: Passphrase, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface +} \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java index d62d5bad..e5e452f2 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java @@ -149,7 +149,7 @@ public class KeyRingInfoTest { private static PGPSecretKeyRing encryptSecretKeys(PGPSecretKeyRing secretKeys) throws PGPException { return PGPainless.modifyKeyRing(secretKeys) - .changePassphraseFromOldPassphrase(null) + .changePassphraseFromOldPassphrase(Passphrase.emptyPassphrase()) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("sw0rdf1sh")) .done(); 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 19f84930..7e15c998 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 @@ -12,9 +12,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.util.Date; import java.util.Iterator; import java.util.NoSuchElementException; +import openpgp.DateExtensionsKt; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; @@ -113,16 +115,17 @@ public class AddUserIdTest { @Test public void addNewPrimaryUserIdTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + Date now = new Date(); PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("Alice"); UserId bob = UserId.newBuilder().withName("Bob").noEmail().noComment().build(); assertNotEquals("Bob", PGPainless.inspectKeyRing(secretKeys).getPrimaryUserId()); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = PGPainless.modifyKeyRing(secretKeys, DateExtensionsKt.plusSeconds(now, 1)) .addPrimaryUserId(bob, SecretKeyRingProtector.unprotectedKeys()) .done(); - assertEquals("Bob", PGPainless.inspectKeyRing(secretKeys).getPrimaryUserId()); + assertEquals("Bob", PGPainless.inspectKeyRing(secretKeys, DateExtensionsKt.plusSeconds(now, 2)).getPrimaryUserId()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java index f324892f..ba6673e5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java @@ -84,7 +84,7 @@ public class S2KUsageFixTest { } PGPSecretKeyRing after = PGPainless.modifyKeyRing(unprotected) - .changePassphraseFromOldPassphrase(null) + .changePassphraseFromOldPassphrase(Passphrase.emptyPassphrase()) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("after")) .done();