001// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.key.modification.secretkeyring;
006
007import static org.pgpainless.util.CollectionUtils.concat;
008
009import java.io.IOException;
010import java.security.InvalidAlgorithmParameterException;
011import java.security.NoSuchAlgorithmException;
012import java.util.ArrayList;
013import java.util.Collections;
014import java.util.Date;
015import java.util.Iterator;
016import java.util.List;
017import java.util.Map;
018import java.util.NoSuchElementException;
019import java.util.Set;
020import javax.annotation.Nonnull;
021import javax.annotation.Nullable;
022
023import org.bouncycastle.bcpg.S2K;
024import org.bouncycastle.bcpg.SecretKeyPacket;
025import org.bouncycastle.bcpg.sig.KeyExpirationTime;
026import org.bouncycastle.openpgp.PGPException;
027import org.bouncycastle.openpgp.PGPKeyPair;
028import org.bouncycastle.openpgp.PGPKeyRingGenerator;
029import org.bouncycastle.openpgp.PGPPublicKey;
030import org.bouncycastle.openpgp.PGPSecretKey;
031import org.bouncycastle.openpgp.PGPSecretKeyRing;
032import org.bouncycastle.openpgp.PGPSignature;
033import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
034import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor;
035import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
036import org.pgpainless.PGPainless;
037import org.pgpainless.algorithm.AlgorithmSuite;
038import org.pgpainless.algorithm.CompressionAlgorithm;
039import org.pgpainless.algorithm.Feature;
040import org.pgpainless.algorithm.HashAlgorithm;
041import org.pgpainless.algorithm.KeyFlag;
042import org.pgpainless.algorithm.PublicKeyAlgorithm;
043import org.pgpainless.algorithm.SignatureType;
044import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
045import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator;
046import org.pgpainless.implementation.ImplementationFactory;
047import org.pgpainless.key.generation.KeyRingBuilder;
048import org.pgpainless.key.generation.KeySpec;
049import org.pgpainless.key.info.KeyRingInfo;
050import org.pgpainless.key.protection.CachingSecretKeyRingProtector;
051import org.pgpainless.key.protection.KeyRingProtectionSettings;
052import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector;
053import org.pgpainless.key.protection.SecretKeyRingProtector;
054import org.pgpainless.key.protection.UnprotectedKeysProtector;
055import org.pgpainless.key.protection.fixes.S2KUsageFix;
056import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider;
057import org.pgpainless.key.util.KeyRingUtils;
058import org.pgpainless.key.util.RevocationAttributes;
059import org.pgpainless.signature.builder.DirectKeySignatureBuilder;
060import org.pgpainless.signature.builder.RevocationSignatureBuilder;
061import org.pgpainless.signature.builder.SelfSignatureBuilder;
062import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets;
063import org.pgpainless.signature.subpackets.SelfSignatureSubpackets;
064import org.pgpainless.signature.subpackets.SignatureSubpackets;
065import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper;
066import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil;
067import org.pgpainless.util.BCUtil;
068import org.pgpainless.util.CollectionUtils;
069import org.pgpainless.util.Passphrase;
070import org.pgpainless.util.selection.userid.SelectUserId;
071
072public class SecretKeyRingEditor implements SecretKeyRingEditorInterface {
073
074    private PGPSecretKeyRing secretKeyRing;
075
076    public SecretKeyRingEditor(PGPSecretKeyRing secretKeyRing) {
077        if (secretKeyRing == null) {
078            throw new NullPointerException("SecretKeyRing MUST NOT be null.");
079        }
080        this.secretKeyRing = secretKeyRing;
081    }
082
083    @Override
084    public SecretKeyRingEditorInterface addUserId(
085            @Nonnull CharSequence userId,
086            @Nonnull SecretKeyRingProtector secretKeyRingProtector)
087            throws PGPException {
088        return addUserId(userId, null, secretKeyRingProtector);
089    }
090
091    @Override
092    public SecretKeyRingEditorInterface addUserId(
093            @Nonnull CharSequence userId,
094            @Nullable SelfSignatureSubpackets.Callback signatureSubpacketCallback,
095            @Nonnull SecretKeyRingProtector protector)
096            throws PGPException {
097        String sanitizeUserId = sanitizeUserId(userId);
098
099        // user-id certifications live on the primary key
100        PGPSecretKey primaryKey = secretKeyRing.getSecretKey();
101
102        // retain key flags from previous signature
103        KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing);
104        if (info.isHardRevoked(userId.toString())) {
105            throw new IllegalArgumentException("User-ID " + userId + " is hard revoked and cannot be re-certified.");
106        }
107        List<KeyFlag> keyFlags = info.getKeyFlagsOf(info.getKeyId());
108
109        Set<HashAlgorithm> hashAlgorithmPreferences;
110        Set<SymmetricKeyAlgorithm> symmetricKeyAlgorithmPreferences;
111        Set<CompressionAlgorithm> compressionAlgorithmPreferences;
112        try {
113            hashAlgorithmPreferences = info.getPreferredHashAlgorithms();
114            symmetricKeyAlgorithmPreferences = info.getPreferredSymmetricKeyAlgorithms();
115            compressionAlgorithmPreferences = info.getPreferredCompressionAlgorithms();
116        } catch (IllegalStateException e) {
117            // missing user-id sig
118            AlgorithmSuite algorithmSuite = AlgorithmSuite.getDefaultAlgorithmSuite();
119            hashAlgorithmPreferences = algorithmSuite.getHashAlgorithms();
120            symmetricKeyAlgorithmPreferences = algorithmSuite.getSymmetricKeyAlgorithms();
121            compressionAlgorithmPreferences = algorithmSuite.getCompressionAlgorithms();
122        }
123
124        SelfSignatureBuilder builder = new SelfSignatureBuilder(primaryKey, protector);
125        builder.setSignatureType(SignatureType.POSITIVE_CERTIFICATION);
126
127        // Retain signature subpackets of previous signatures
128        builder.getHashedSubpackets().setKeyFlags(keyFlags);
129        builder.getHashedSubpackets().setPreferredHashAlgorithms(hashAlgorithmPreferences);
130        builder.getHashedSubpackets().setPreferredSymmetricKeyAlgorithms(symmetricKeyAlgorithmPreferences);
131        builder.getHashedSubpackets().setPreferredCompressionAlgorithms(compressionAlgorithmPreferences);
132        builder.getHashedSubpackets().setFeatures(Feature.MODIFICATION_DETECTION);
133
134        builder.applyCallback(signatureSubpacketCallback);
135
136        PGPSignature signature = builder.build(primaryKey.getPublicKey(), sanitizeUserId);
137        secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, sanitizeUserId, signature);
138
139        return this;
140    }
141
142    @Override
143    public SecretKeyRingEditorInterface addPrimaryUserId(
144            @Nonnull CharSequence userId, @Nonnull SecretKeyRingProtector protector)
145            throws PGPException {
146
147        // Determine previous key expiration date
148        PGPPublicKey primaryKey = secretKeyRing.getSecretKey().getPublicKey();
149        KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing);
150        String primaryUserId = info.getPrimaryUserId();
151        PGPSignature signature = primaryUserId == null ?
152                info.getLatestDirectKeySelfSignature() : info.getLatestUserIdCertification(primaryUserId);
153        final Date previousKeyExpiration = signature == null ? null :
154            SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(signature, primaryKey);
155
156        // Add new primary user-id signature
157        addUserId(
158                userId,
159                new SelfSignatureSubpackets.Callback() {
160                    @Override
161                    public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) {
162                        hashedSubpackets.setPrimaryUserId();
163                        if (previousKeyExpiration != null) {
164                            hashedSubpackets.setKeyExpirationTime(primaryKey, previousKeyExpiration);
165                        } else {
166                            hashedSubpackets.setKeyExpirationTime(null);
167                        }
168                    }
169                },
170                protector);
171
172        // unmark previous primary user-ids to be non-primary
173        info = PGPainless.inspectKeyRing(secretKeyRing);
174        for (String otherUserId : info.getValidAndExpiredUserIds()) {
175            if (userId.toString().equals(otherUserId)) {
176                continue;
177            }
178
179            // We need to unmark this user-id as primary
180            if (info.getLatestUserIdCertification(otherUserId).getHashedSubPackets().isPrimaryUserID()) {
181                addUserId(otherUserId, new SelfSignatureSubpackets.Callback() {
182                    @Override
183                    public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) {
184                        hashedSubpackets.setPrimaryUserId(null);
185                        hashedSubpackets.setKeyExpirationTime(null); // non-primary
186                    }
187                }, protector);
188            }
189        }
190        return this;
191    }
192
193    @Override
194    public SecretKeyRingEditorInterface removeUserId(
195            SelectUserId userIdSelector,
196            SecretKeyRingProtector protector)
197            throws PGPException {
198        RevocationAttributes revocationAttributes = RevocationAttributes.createCertificateRevocation()
199                .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID)
200                .withoutDescription();
201        return revokeUserIds(userIdSelector,
202                protector,
203                revocationAttributes);
204    }
205
206    @Override
207    public SecretKeyRingEditorInterface removeUserId(
208            CharSequence userId,
209            SecretKeyRingProtector protector) throws PGPException {
210        return removeUserId(
211                SelectUserId.exactMatch(userId.toString()),
212                protector);
213    }
214
215    // TODO: Move to utility class?
216    private String sanitizeUserId(@Nonnull CharSequence userId) {
217        // TODO: Further research how to sanitize user IDs.
218        //  eg. what about newlines?
219        return userId.toString().trim();
220    }
221
222    @Override
223    public SecretKeyRingEditorInterface addSubKey(
224            @Nonnull KeySpec keySpec,
225            @Nonnull Passphrase subKeyPassphrase,
226            @Nonnull SecretKeyRingProtector secretKeyRingProtector)
227            throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException {
228        PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(keySpec);
229
230        SecretKeyRingProtector subKeyProtector = PasswordBasedSecretKeyRingProtector
231                .forKeyId(keyPair.getKeyID(), subKeyPassphrase);
232
233        SelfSignatureSubpackets.Callback callback = new SelfSignatureSubpackets.Callback() {
234            @Override
235            public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) {
236                SignatureSubpacketsHelper.applyFrom(keySpec.getSubpackets(), (SignatureSubpackets) hashedSubpackets);
237            }
238        };
239
240        List<KeyFlag> keyFlags = KeyFlag.fromBitmask(keySpec.getSubpackets().getKeyFlags());
241        KeyFlag firstFlag = keyFlags.remove(0);
242        KeyFlag[] otherFlags = keyFlags.toArray(new KeyFlag[0]);
243
244        return addSubKey(keyPair, callback, subKeyProtector, secretKeyRingProtector, firstFlag, otherFlags);
245    }
246
247    @Override
248    public SecretKeyRingEditorInterface addSubKey(
249            @Nonnull KeySpec keySpec,
250            @Nullable Passphrase subkeyPassphrase,
251            @Nullable SelfSignatureSubpackets.Callback subpacketsCallback,
252            @Nonnull SecretKeyRingProtector secretKeyRingProtector)
253            throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
254        PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(keySpec);
255
256        SecretKeyRingProtector subKeyProtector = PasswordBasedSecretKeyRingProtector
257                .forKeyId(keyPair.getKeyID(), subkeyPassphrase);
258
259        List<KeyFlag> keyFlags = KeyFlag.fromBitmask(keySpec.getSubpackets().getKeyFlags());
260        KeyFlag firstFlag = keyFlags.remove(0);
261        KeyFlag[] otherFlags = keyFlags.toArray(new KeyFlag[0]);
262
263        return addSubKey(keyPair, subpacketsCallback, subKeyProtector, secretKeyRingProtector, firstFlag, otherFlags);
264    }
265
266    @Override
267    public SecretKeyRingEditorInterface addSubKey(
268            @Nonnull PGPKeyPair subkey,
269            @Nullable SelfSignatureSubpackets.Callback bindingSignatureCallback,
270            @Nonnull SecretKeyRingProtector subkeyProtector,
271            @Nonnull SecretKeyRingProtector primaryKeyProtector,
272            @Nonnull KeyFlag keyFlag,
273            KeyFlag... additionalKeyFlags)
274            throws PGPException, IOException, NoSuchAlgorithmException {
275        KeyFlag[] flags = concat(keyFlag, additionalKeyFlags);
276        PublicKeyAlgorithm subkeyAlgorithm = PublicKeyAlgorithm.fromId(subkey.getPublicKey().getAlgorithm());
277        SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm);
278
279        // check key against public key algorithm policy
280        PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.fromId(subkey.getPublicKey().getAlgorithm());
281        int bitStrength = BCUtil.getBitStrength(subkey.getPublicKey());
282        if (!PGPainless.getPolicy().getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) {
283            throw new IllegalArgumentException("Public key algorithm policy violation: " +
284                    publicKeyAlgorithm + " with bit strength " + bitStrength + " is not acceptable.");
285        }
286
287        PGPSecretKey primaryKey = secretKeyRing.getSecretKey();
288        KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing);
289        PublicKeyAlgorithm signingKeyAlgorithm = PublicKeyAlgorithm.fromId(primaryKey.getPublicKey().getAlgorithm());
290        HashAlgorithm hashAlgorithm = HashAlgorithmNegotiator
291                .negotiateSignatureHashAlgorithm(PGPainless.getPolicy())
292                .negotiateHashAlgorithm(info.getPreferredHashAlgorithms());
293
294        // While we'd like to rely on our own BindingSignatureBuilder implementation,
295        //  unfortunately we have to use BCs PGPKeyRingGenerator class since there is no public constructor
296        //  for subkeys. See https://github.com/bcgit/bc-java/pull/1063
297        PGPKeyRingGenerator ringGenerator = new PGPKeyRingGenerator(
298                secretKeyRing,
299                primaryKeyProtector.getDecryptor(primaryKey.getKeyID()),
300                ImplementationFactory.getInstance().getV4FingerprintCalculator(),
301                ImplementationFactory.getInstance().getPGPContentSignerBuilder(
302                        signingKeyAlgorithm, hashAlgorithm),
303                subkeyProtector.getEncryptor(subkey.getKeyID()));
304
305        SelfSignatureSubpackets hashedSubpackets = SignatureSubpackets.createHashedSubpackets(primaryKey.getPublicKey());
306        SelfSignatureSubpackets unhashedSubpackets = SignatureSubpackets.createEmptySubpackets();
307        hashedSubpackets.setKeyFlags(flags);
308
309        if (bindingSignatureCallback != null) {
310            bindingSignatureCallback.modifyHashedSubpackets(hashedSubpackets);
311            bindingSignatureCallback.modifyUnhashedSubpackets(unhashedSubpackets);
312        }
313
314        boolean isSigningKey = CollectionUtils.contains(flags, KeyFlag.SIGN_DATA) ||
315                CollectionUtils.contains(flags, KeyFlag.CERTIFY_OTHER);
316        PGPContentSignerBuilder primaryKeyBindingSigner = null;
317        if (isSigningKey) {
318            primaryKeyBindingSigner = ImplementationFactory.getInstance().getPGPContentSignerBuilder(subkeyAlgorithm, hashAlgorithm);
319        }
320
321        ringGenerator.addSubKey(subkey,
322                SignatureSubpacketsHelper.toVector((SignatureSubpackets) hashedSubpackets),
323                SignatureSubpacketsHelper.toVector((SignatureSubpackets) unhashedSubpackets),
324                primaryKeyBindingSigner);
325
326        secretKeyRing = ringGenerator.generateSecretKeyRing();
327
328        return this;
329    }
330
331    @Override
332    public SecretKeyRingEditorInterface revoke(@Nonnull SecretKeyRingProtector secretKeyRingProtector,
333                                               @Nullable RevocationAttributes revocationAttributes)
334            throws PGPException {
335        RevocationSignatureSubpackets.Callback callback = callbackFromRevocationAttributes(revocationAttributes);
336        return revoke(secretKeyRingProtector, callback);
337    }
338
339    @Override
340    public SecretKeyRingEditorInterface revoke(@Nonnull SecretKeyRingProtector secretKeyRingProtector,
341                                               @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback)
342            throws PGPException {
343        return revokeSubKey(secretKeyRing.getSecretKey().getKeyID(), secretKeyRingProtector, subpacketsCallback);
344    }
345
346    @Override
347    public SecretKeyRingEditorInterface revokeSubKey(long subKeyId,
348                                                     SecretKeyRingProtector protector,
349                                                     RevocationAttributes revocationAttributes)
350            throws PGPException {
351        RevocationSignatureSubpackets.Callback callback = callbackFromRevocationAttributes(revocationAttributes);
352        return revokeSubKey(subKeyId, protector, callback);
353    }
354
355    @Override
356    public SecretKeyRingEditorInterface revokeSubKey(long keyID,
357                                                     @Nonnull SecretKeyRingProtector secretKeyRingProtector,
358                                                     @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback)
359            throws PGPException {
360        // retrieve subkey to be revoked
361        PGPPublicKey revokeeSubKey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, keyID);
362        // create revocation
363        PGPSignature subKeyRevocation = generateRevocation(secretKeyRingProtector, revokeeSubKey,
364                subpacketsCallback);
365        // inject revocation sig into key ring
366        secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, revokeeSubKey, subKeyRevocation);
367        return this;
368    }
369
370    @Override
371    public PGPSignature createRevocationCertificate(@Nonnull SecretKeyRingProtector secretKeyRingProtector,
372                                                    @Nullable RevocationAttributes revocationAttributes)
373            throws PGPException {
374        PGPPublicKey revokeeSubKey = secretKeyRing.getPublicKey();
375        PGPSignature revocationCertificate = generateRevocation(
376                secretKeyRingProtector, revokeeSubKey, callbackFromRevocationAttributes(revocationAttributes));
377        return revocationCertificate;
378    }
379
380    @Override
381    public PGPSignature createRevocationCertificate(
382            long subkeyId,
383            @Nonnull SecretKeyRingProtector secretKeyRingProtector,
384            @Nullable RevocationAttributes revocationAttributes)
385            throws PGPException {
386        PGPPublicKey revokeeSubkey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, subkeyId);
387        RevocationSignatureSubpackets.Callback callback = callbackFromRevocationAttributes(revocationAttributes);
388        return generateRevocation(secretKeyRingProtector, revokeeSubkey, callback);
389    }
390
391    @Override
392    public PGPSignature createRevocationCertificate(
393            long subkeyId,
394            @Nonnull SecretKeyRingProtector secretKeyRingProtector,
395            @Nullable RevocationSignatureSubpackets.Callback certificateSubpacketsCallback)
396            throws PGPException {
397        PGPPublicKey revokeeSubkey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, subkeyId);
398        return generateRevocation(secretKeyRingProtector, revokeeSubkey, certificateSubpacketsCallback);
399    }
400
401    private PGPSignature generateRevocation(@Nonnull SecretKeyRingProtector protector,
402                                            @Nonnull PGPPublicKey revokeeSubKey,
403                                            @Nullable RevocationSignatureSubpackets.Callback callback)
404            throws PGPException {
405        PGPSecretKey primaryKey = secretKeyRing.getSecretKey();
406        SignatureType signatureType = revokeeSubKey.isMasterKey() ?
407                SignatureType.KEY_REVOCATION : SignatureType.SUBKEY_REVOCATION;
408
409        RevocationSignatureBuilder signatureBuilder =
410                new RevocationSignatureBuilder(signatureType, primaryKey, protector);
411        signatureBuilder.applyCallback(callback);
412        PGPSignature revocation = signatureBuilder.build(revokeeSubKey);
413        return revocation;
414    }
415
416    private static RevocationSignatureSubpackets.Callback callbackFromRevocationAttributes(
417            @Nullable RevocationAttributes attributes) {
418        return new RevocationSignatureSubpackets.Callback() {
419            @Override
420            public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) {
421                if (attributes != null) {
422                    hashedSubpackets.setRevocationReason(attributes);
423                }
424            }
425        };
426    }
427
428    @Override
429    public SecretKeyRingEditorInterface revokeUserId(
430            @Nonnull CharSequence userId,
431            @Nonnull SecretKeyRingProtector secretKeyRingProtector,
432            @Nullable RevocationAttributes revocationAttributes)
433            throws PGPException {
434        if (revocationAttributes != null) {
435            RevocationAttributes.Reason reason = revocationAttributes.getReason();
436            if (reason != RevocationAttributes.Reason.NO_REASON
437                    && reason != RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) {
438                throw new IllegalArgumentException("Revocation reason must either be NO_REASON or USER_ID_NO_LONGER_VALID");
439            }
440        }
441
442        RevocationSignatureSubpackets.Callback callback = new RevocationSignatureSubpackets.Callback() {
443            @Override
444            public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) {
445                if (revocationAttributes != null) {
446                    hashedSubpackets.setRevocationReason(false, revocationAttributes);
447                }
448            }
449        };
450
451        return revokeUserId(userId, secretKeyRingProtector, callback);
452    }
453
454    @Override
455    public SecretKeyRingEditorInterface revokeUserId(
456            @Nonnull CharSequence userId,
457            @Nonnull SecretKeyRingProtector secretKeyRingProtector,
458            @Nullable RevocationSignatureSubpackets.Callback subpacketCallback)
459            throws PGPException {
460        String sanitized = sanitizeUserId(userId);
461        return revokeUserIds(
462                SelectUserId.exactMatch(sanitized),
463                secretKeyRingProtector,
464                subpacketCallback);
465    }
466
467    @Override
468    public SecretKeyRingEditorInterface revokeUserIds(
469            @Nonnull SelectUserId userIdSelector,
470            @Nonnull SecretKeyRingProtector secretKeyRingProtector,
471            @Nullable RevocationAttributes revocationAttributes)
472            throws PGPException {
473
474        return revokeUserIds(
475                userIdSelector,
476                secretKeyRingProtector,
477                new RevocationSignatureSubpackets.Callback() {
478                    @Override
479                    public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) {
480                        hashedSubpackets.setRevocationReason(revocationAttributes);
481                    }
482                });
483    }
484
485    @Override
486    public SecretKeyRingEditorInterface revokeUserIds(
487            @Nonnull SelectUserId userIdSelector,
488            @Nonnull SecretKeyRingProtector secretKeyRingProtector,
489            @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback)
490            throws PGPException {
491        List<String> selected = userIdSelector.selectUserIds(secretKeyRing);
492        if (selected.isEmpty()) {
493            throw new NoSuchElementException("No matching user-ids found on the key.");
494        }
495
496        for (String userId : selected) {
497            doRevokeUserId(userId, secretKeyRingProtector, subpacketsCallback);
498        }
499
500        return this;
501    }
502
503    private SecretKeyRingEditorInterface doRevokeUserId(
504            @Nonnull String userId,
505            @Nonnull SecretKeyRingProtector protector,
506            @Nullable RevocationSignatureSubpackets.Callback callback)
507            throws PGPException {
508        PGPSecretKey primarySecretKey = secretKeyRing.getSecretKey();
509        RevocationSignatureBuilder signatureBuilder = new RevocationSignatureBuilder(
510                SignatureType.CERTIFICATION_REVOCATION,
511                primarySecretKey,
512                protector);
513
514        signatureBuilder.applyCallback(callback);
515
516        PGPSignature revocationSignature = signatureBuilder.build(userId);
517        secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, userId, revocationSignature);
518        return this;
519    }
520
521    @Override
522    public SecretKeyRingEditorInterface setExpirationDate(
523            @Nullable Date expiration,
524            @Nonnull SecretKeyRingProtector secretKeyRingProtector)
525            throws PGPException {
526
527        PGPSecretKey primaryKey = secretKeyRing.getSecretKey();
528        if (!primaryKey.isMasterKey()) {
529            throw new IllegalArgumentException("Key Ring does not appear to contain a primary secret key.");
530        }
531
532        // reissue direct key sig
533        PGPSignature prevDirectKeySig = getPreviousDirectKeySignature();
534        if (prevDirectKeySig != null) {
535            PGPSignature directKeySig = reissueDirectKeySignature(expiration, secretKeyRingProtector, prevDirectKeySig);
536            secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryKey.getPublicKey(), directKeySig);
537        }
538
539        // reissue primary user-id sig
540        String primaryUserId = PGPainless.inspectKeyRing(secretKeyRing).getPossiblyExpiredPrimaryUserId();
541        if (primaryUserId != null) {
542            PGPSignature prevUserIdSig = getPreviousUserIdSignatures(primaryUserId);
543            PGPSignature userIdSig = reissuePrimaryUserIdSig(expiration, secretKeyRingProtector, primaryUserId, prevUserIdSig);
544            secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryUserId, userIdSig);
545        }
546
547        KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing);
548        for (String userId : info.getValidUserIds()) {
549            if (userId.equals(primaryUserId)) {
550                continue;
551            }
552
553            PGPSignature prevUserIdSig = info.getLatestUserIdCertification(userId);
554            if (prevUserIdSig == null) {
555                throw new AssertionError("A valid user-id shall never have no user-id signature.");
556            }
557
558            if (prevUserIdSig.getHashedSubPackets().isPrimaryUserID()) {
559                PGPSignature userIdSig = reissueNonPrimaryUserId(secretKeyRingProtector, userId, prevUserIdSig);
560                secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryUserId, userIdSig);
561            }
562        }
563
564        return this;
565    }
566
567    private PGPSignature reissueNonPrimaryUserId(
568            SecretKeyRingProtector secretKeyRingProtector,
569            String userId,
570            PGPSignature prevUserIdSig)
571            throws PGPException {
572        SelfSignatureBuilder builder = new SelfSignatureBuilder(secretKeyRing.getSecretKey(), secretKeyRingProtector, prevUserIdSig);
573        builder.applyCallback(new SelfSignatureSubpackets.Callback() {
574            @Override
575            public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) {
576                // unmark as primary
577                hashedSubpackets.setPrimaryUserId(null);
578            }
579        });
580        return builder.build(secretKeyRing.getPublicKey(), userId);
581    }
582
583    private PGPSignature reissuePrimaryUserIdSig(
584            @Nullable Date expiration,
585            @Nonnull SecretKeyRingProtector secretKeyRingProtector,
586            @Nonnull String primaryUserId,
587            @Nonnull PGPSignature prevUserIdSig)
588            throws PGPException {
589        PGPSecretKey primaryKey = secretKeyRing.getSecretKey();
590        PGPPublicKey publicKey = primaryKey.getPublicKey();
591
592        SelfSignatureBuilder builder = new SelfSignatureBuilder(primaryKey, secretKeyRingProtector, prevUserIdSig);
593        builder.applyCallback(new SelfSignatureSubpackets.Callback() {
594            @Override
595            public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) {
596                if (expiration != null) {
597                    hashedSubpackets.setKeyExpirationTime(true, publicKey.getCreationTime(), expiration);
598                } else {
599                    hashedSubpackets.setKeyExpirationTime(new KeyExpirationTime(true, 0));
600                }
601                hashedSubpackets.setPrimaryUserId();
602            }
603        });
604        return builder.build(publicKey, primaryUserId);
605    }
606
607    private PGPSignature reissueDirectKeySignature(
608            Date expiration,
609            SecretKeyRingProtector secretKeyRingProtector,
610            PGPSignature prevDirectKeySig)
611            throws PGPException {
612        PGPSecretKey primaryKey = secretKeyRing.getSecretKey();
613        PGPPublicKey publicKey = primaryKey.getPublicKey();
614        final Date keyCreationTime = publicKey.getCreationTime();
615
616        DirectKeySignatureBuilder builder = new DirectKeySignatureBuilder(primaryKey, secretKeyRingProtector, prevDirectKeySig);
617        builder.applyCallback(new SelfSignatureSubpackets.Callback() {
618            @Override
619            public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) {
620                if (expiration != null) {
621                    hashedSubpackets.setKeyExpirationTime(keyCreationTime, expiration);
622                } else {
623                    hashedSubpackets.setKeyExpirationTime(null);
624                }
625            }
626        });
627
628        return builder.build(publicKey);
629    }
630
631    private PGPSignature getPreviousDirectKeySignature() {
632        KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing);
633        return info.getLatestDirectKeySelfSignature();
634    }
635
636    private PGPSignature getPreviousUserIdSignatures(String userId) {
637        KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing);
638        return info.getLatestUserIdCertification(userId);
639    }
640
641    @Override
642    public WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase(
643            @Nullable Passphrase oldPassphrase,
644            @Nonnull KeyRingProtectionSettings oldProtectionSettings) {
645        SecretKeyRingProtector protector = new PasswordBasedSecretKeyRingProtector(
646                oldProtectionSettings,
647                new SolitaryPassphraseProvider(oldPassphrase));
648
649        return new WithKeyRingEncryptionSettingsImpl(null, protector);
650    }
651
652    @Override
653    public WithKeyRingEncryptionSettings changeSubKeyPassphraseFromOldPassphrase(
654            @Nonnull Long keyId,
655            @Nullable Passphrase oldPassphrase,
656            @Nonnull KeyRingProtectionSettings oldProtectionSettings) {
657        Map<Long, Passphrase> passphraseMap = Collections.singletonMap(keyId, oldPassphrase);
658        SecretKeyRingProtector protector = new CachingSecretKeyRingProtector(
659                passphraseMap, oldProtectionSettings, null);
660
661        return new WithKeyRingEncryptionSettingsImpl(keyId, protector);
662    }
663
664    @Override
665    public PGPSecretKeyRing done() {
666        return secretKeyRing;
667    }
668
669    private final class WithKeyRingEncryptionSettingsImpl implements WithKeyRingEncryptionSettings {
670
671        private final Long keyId;
672        // Protector to unlock the key with the old passphrase
673        private final SecretKeyRingProtector oldProtector;
674
675        /**
676         * Builder for selecting protection settings.
677         *
678         * If the keyId is null, the whole keyRing will get the same new passphrase.
679         *
680         * @param keyId id of the subkey whose passphrase will be changed, or null.
681         * @param oldProtector protector do unlock the key/ring.
682         */
683        private WithKeyRingEncryptionSettingsImpl(Long keyId, SecretKeyRingProtector oldProtector) {
684            this.keyId = keyId;
685            this.oldProtector = oldProtector;
686        }
687
688        @Override
689        public WithPassphrase withSecureDefaultSettings() {
690            return withCustomSettings(KeyRingProtectionSettings.secureDefaultSettings());
691        }
692
693        @Override
694        public WithPassphrase withCustomSettings(KeyRingProtectionSettings settings) {
695            return new WithPassphraseImpl(keyId, oldProtector, settings);
696        }
697    }
698
699    private final class WithPassphraseImpl implements WithPassphrase {
700
701        private final SecretKeyRingProtector oldProtector;
702        private final KeyRingProtectionSettings newProtectionSettings;
703        private final Long keyId;
704
705        private WithPassphraseImpl(
706                Long keyId,
707                SecretKeyRingProtector oldProtector,
708                KeyRingProtectionSettings newProtectionSettings) {
709            this.keyId = keyId;
710            this.oldProtector = oldProtector;
711            this.newProtectionSettings = newProtectionSettings;
712        }
713
714        @Override
715        public SecretKeyRingEditorInterface toNewPassphrase(Passphrase passphrase)
716                throws PGPException {
717            SecretKeyRingProtector newProtector = new PasswordBasedSecretKeyRingProtector(
718                    newProtectionSettings, new SolitaryPassphraseProvider(passphrase));
719
720            PGPSecretKeyRing secretKeys = changePassphrase(
721                    keyId, SecretKeyRingEditor.this.secretKeyRing, oldProtector, newProtector);
722            SecretKeyRingEditor.this.secretKeyRing = secretKeys;
723
724            return SecretKeyRingEditor.this;
725        }
726
727        @Override
728        public SecretKeyRingEditorInterface toNoPassphrase()
729                throws PGPException {
730            SecretKeyRingProtector newProtector = new UnprotectedKeysProtector();
731
732            PGPSecretKeyRing secretKeys = changePassphrase(
733                    keyId, SecretKeyRingEditor.this.secretKeyRing, oldProtector, newProtector);
734            SecretKeyRingEditor.this.secretKeyRing = secretKeys;
735
736            return SecretKeyRingEditor.this;
737        }
738    }
739
740    private PGPSecretKeyRing changePassphrase(Long keyId,
741                                              PGPSecretKeyRing secretKeys,
742                                              SecretKeyRingProtector oldProtector,
743                                              SecretKeyRingProtector newProtector)
744            throws PGPException {
745        List<PGPSecretKey> secretKeyList = new ArrayList<>();
746        if (keyId == null) {
747            // change passphrase of whole key ring
748            Iterator<PGPSecretKey> secretKeyIterator = secretKeys.getSecretKeys();
749            while (secretKeyIterator.hasNext()) {
750                PGPSecretKey secretKey = secretKeyIterator.next();
751                secretKey = reencryptPrivateKey(secretKey, oldProtector, newProtector);
752                secretKeyList.add(secretKey);
753            }
754        } else {
755            // change passphrase of selected subkey only
756            Iterator<PGPSecretKey> secretKeyIterator = secretKeys.getSecretKeys();
757            while (secretKeyIterator.hasNext()) {
758                PGPSecretKey secretKey = secretKeyIterator.next();
759                if (secretKey.getPublicKey().getKeyID() == keyId) {
760                    // Re-encrypt only the selected subkey
761                    secretKey = reencryptPrivateKey(secretKey, oldProtector, newProtector);
762                }
763                secretKeyList.add(secretKey);
764            }
765        }
766
767        PGPSecretKeyRing newRing = new PGPSecretKeyRing(secretKeyList);
768        newRing = s2kUsageFixIfNecessary(newRing, newProtector);
769        return newRing;
770    }
771
772    private PGPSecretKeyRing s2kUsageFixIfNecessary(PGPSecretKeyRing secretKeys, SecretKeyRingProtector protector)
773            throws PGPException {
774        boolean hasS2KUsageChecksum = false;
775        for (PGPSecretKey secKey : secretKeys) {
776            if (secKey.getS2KUsage() == SecretKeyPacket.USAGE_CHECKSUM) {
777                hasS2KUsageChecksum = true;
778                break;
779            }
780        }
781        if (hasS2KUsageChecksum) {
782            secretKeys = S2KUsageFix.replaceUsageChecksumWithUsageSha1(
783                    secretKeys, protector, true);
784        }
785        return secretKeys;
786    }
787
788    private static PGPSecretKey reencryptPrivateKey(
789            PGPSecretKey secretKey,
790            SecretKeyRingProtector oldProtector,
791            SecretKeyRingProtector newProtector)
792            throws PGPException {
793        S2K s2k = secretKey.getS2K();
794        // If the key uses GNU_DUMMY_S2K, we leave it as is and skip this block
795        if (s2k == null || s2k.getType() != S2K.GNU_DUMMY_S2K) {
796            long secretKeyId = secretKey.getKeyID();
797            PBESecretKeyDecryptor decryptor = oldProtector.getDecryptor(secretKeyId);
798            PBESecretKeyEncryptor encryptor = newProtector.getEncryptor(secretKeyId);
799            secretKey = PGPSecretKey.copyWithNewPassword(secretKey, decryptor, encryptor);
800        }
801        return secretKey;
802    }
803}