001// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>, 2021 Flowcrypt a.s.
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.key.info;
006
007import static org.pgpainless.util.CollectionUtils.iteratorToList;
008
009import java.util.ArrayList;
010import java.util.Collections;
011import java.util.Date;
012import java.util.HashMap;
013import java.util.HashSet;
014import java.util.Iterator;
015import java.util.List;
016import java.util.Map;
017import java.util.NoSuchElementException;
018import java.util.Set;
019import java.util.regex.Matcher;
020import java.util.regex.Pattern;
021import javax.annotation.Nonnull;
022import javax.annotation.Nullable;
023
024import org.bouncycastle.bcpg.sig.PrimaryUserID;
025import org.bouncycastle.bcpg.sig.RevocationReason;
026import org.bouncycastle.openpgp.PGPKeyRing;
027import org.bouncycastle.openpgp.PGPPublicKey;
028import org.bouncycastle.openpgp.PGPPublicKeyRing;
029import org.bouncycastle.openpgp.PGPSecretKey;
030import org.bouncycastle.openpgp.PGPSecretKeyRing;
031import org.bouncycastle.openpgp.PGPSignature;
032import org.pgpainless.PGPainless;
033import org.pgpainless.algorithm.CompressionAlgorithm;
034import org.pgpainless.algorithm.EncryptionPurpose;
035import org.pgpainless.algorithm.HashAlgorithm;
036import org.pgpainless.algorithm.KeyFlag;
037import org.pgpainless.algorithm.PublicKeyAlgorithm;
038import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
039import org.pgpainless.exception.KeyValidationError;
040import org.pgpainless.key.OpenPgpFingerprint;
041import org.pgpainless.key.SubkeyIdentifier;
042import org.pgpainless.key.util.RevocationAttributes;
043import org.pgpainless.policy.Policy;
044import org.pgpainless.signature.SignatureUtils;
045import org.pgpainless.signature.consumer.SignaturePicker;
046import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil;
047
048/**
049 * Utility class to quickly extract certain information from a {@link PGPPublicKeyRing}/{@link PGPSecretKeyRing}.
050 */
051public class KeyRingInfo {
052
053    private static final Pattern PATTERN_EMAIL = Pattern.compile("[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}");
054
055    private final PGPKeyRing keys;
056    private final Signatures signatures;
057    private final Date evaluationDate;
058    private final String primaryUserId;
059
060    /**
061     * Evaluate the key ring at creation time of the given signature.
062     *
063     * @param keyRing key ring
064     * @param signature signature
065     * @return info of key ring at signature creation time
066     */
067    public static KeyRingInfo evaluateForSignature(PGPKeyRing keyRing, PGPSignature signature) {
068        return new KeyRingInfo(keyRing, signature.getCreationTime());
069    }
070
071    /**
072     * Evaluate the key ring right now.
073     *
074     * @param keys key ring
075     */
076    public KeyRingInfo(PGPKeyRing keys) {
077        this(keys, new Date());
078    }
079
080    /**
081     * Evaluate the key ring at the provided validation date.
082     *
083     * @param keys key ring
084     * @param validationDate date of validation
085     */
086    public KeyRingInfo(PGPKeyRing keys, Date validationDate) {
087        this.keys = keys;
088        this.signatures = new Signatures(keys, validationDate, PGPainless.getPolicy());
089        this.evaluationDate = validationDate;
090        this.primaryUserId = findPrimaryUserId();
091    }
092
093    /**
094     * Return the first {@link PGPPublicKey} of this key ring.
095     *
096     * @return public key
097     */
098    public PGPPublicKey getPublicKey() {
099        return keys.getPublicKey();
100    }
101
102    /**
103     * Return the public key with the given fingerprint.
104     *
105     * @param fingerprint fingerprint
106     * @return public key or null
107     */
108    public @Nullable PGPPublicKey getPublicKey(OpenPgpFingerprint fingerprint) {
109        return getPublicKey(fingerprint.getKeyId());
110    }
111
112    /**
113     * Return the public key with the given key id.
114     *
115     * @param keyId key id
116     * @return public key or null
117     */
118    public @Nullable PGPPublicKey getPublicKey(long keyId) {
119        return getPublicKey(keys, keyId);
120    }
121
122    /**
123     * Return the public key with the given key id from the provided key ring.
124     *
125     * @param keyRing key ring
126     * @param keyId key id
127     * @return public key or null
128     */
129    public static @Nullable PGPPublicKey getPublicKey(PGPKeyRing keyRing, long keyId) {
130        return keyRing.getPublicKey(keyId);
131    }
132
133    /**
134     * Return true if the public key with the given key id is bound to the key ring properly.
135     *
136     * @param keyId key id
137     * @return true if key is bound validly
138     */
139    public boolean isKeyValidlyBound(long keyId) {
140        PGPPublicKey publicKey = keys.getPublicKey(keyId);
141        if (publicKey == null) {
142            return false;
143        }
144
145        if (publicKey == getPublicKey()) {
146            if (signatures.primaryKeyRevocation != null && SignatureUtils.isHardRevocation(signatures.primaryKeyRevocation)) {
147                return false;
148            }
149            return signatures.primaryKeyRevocation == null;
150        }
151
152        PGPSignature binding = signatures.subkeyBindings.get(keyId);
153        PGPSignature revocation = signatures.subkeyRevocations.get(keyId);
154
155        // No valid binding
156        if (binding == null || SignatureUtils.isSignatureExpired(binding)) {
157            return false;
158        }
159
160        // Revocation
161        if (revocation != null) {
162            if (SignatureUtils.isHardRevocation(revocation)) {
163                // Subkey is hard revoked
164                return false;
165            } else {
166                // Key is soft-revoked, not yet re-bound
167                return SignatureUtils.isSignatureExpired(revocation)
168                        || !revocation.getCreationTime().after(binding.getCreationTime());
169            }
170        }
171
172        return true;
173    }
174
175    /**
176     * Return all {@link PGPPublicKey PGPPublicKeys} of this key ring.
177     * The first key in the list being the primary key.
178     * Note that the list is unmodifiable.
179     *
180     * @return list of public keys
181     */
182    public List<PGPPublicKey> getPublicKeys() {
183        Iterator<PGPPublicKey> iterator = keys.getPublicKeys();
184        List<PGPPublicKey> list = iteratorToList(iterator);
185        return Collections.unmodifiableList(list);
186    }
187
188    /**
189     * Return the primary {@link PGPSecretKey} of this key ring or null if the key ring is not a {@link PGPSecretKeyRing}.
190     *
191     * @return primary secret key or null if the key ring is public
192     */
193    public @Nullable PGPSecretKey getSecretKey() {
194        if (keys instanceof PGPSecretKeyRing) {
195            PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) keys;
196            return secretKeys.getSecretKey();
197        }
198        return null;
199    }
200
201    /**
202     * Return the secret key with the given fingerprint.
203     *
204     * @param fingerprint fingerprint
205     * @return secret key or null
206     */
207    public @Nullable PGPSecretKey getSecretKey(OpenPgpFingerprint fingerprint) {
208        return getSecretKey(fingerprint.getKeyId());
209    }
210
211    /**
212     * Return the secret key with the given key id.
213     *
214     * @param keyId key id
215     * @return secret key or null
216     */
217    public @Nullable PGPSecretKey getSecretKey(long keyId) {
218        if (keys instanceof PGPSecretKeyRing) {
219            return ((PGPSecretKeyRing) keys).getSecretKey(keyId);
220        }
221        return null;
222    }
223
224    /**
225     * Return all secret keys of the key ring.
226     * If the key ring is a {@link PGPPublicKeyRing}, then return an empty list.
227     * Note that the list is unmodifiable.
228     *
229     * @return list of secret keys
230     */
231    public List<PGPSecretKey> getSecretKeys() {
232        if (keys instanceof PGPSecretKeyRing) {
233            PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) keys;
234            Iterator<PGPSecretKey> iterator = secretKeys.getSecretKeys();
235            return Collections.unmodifiableList(iteratorToList(iterator));
236        }
237        return Collections.emptyList();
238    }
239
240    /**
241     * Return the key id of the primary key of this key ring.
242     *
243     * @return key id
244     */
245    public long getKeyId() {
246        return getPublicKey().getKeyID();
247    }
248
249    /**
250     * Return the {@link OpenPgpFingerprint} of this key ring.
251     *
252     * @return fingerprint
253     */
254    public OpenPgpFingerprint getFingerprint() {
255        return OpenPgpFingerprint.of(getPublicKey());
256    }
257
258    public @Nullable String getPrimaryUserId() {
259        return primaryUserId;
260    }
261
262    /**
263     * Return the current primary user-id of the key ring.
264     *
265     * Note: If no user-id is marked as primary key using a {@link PrimaryUserID} packet,
266     * this method returns the first user-id on the key, otherwise null.
267     *
268     * @return primary user-id or null
269     */
270    private String findPrimaryUserId() {
271        String primaryUserId = null;
272        Date currentModificationDate = null;
273
274        List<String> userIds = getUserIds();
275        if (userIds.isEmpty()) {
276            return null;
277        }
278
279        String firstUserId = userIds.get(0);
280        if (userIds.size() == 1) {
281            return firstUserId;
282        }
283
284        for (String userId : userIds) {
285            PGPSignature certification = signatures.userIdCertifications.get(userId);
286            if (certification == null) {
287                continue;
288            }
289            Date creationTime = certification.getCreationTime();
290
291            if (certification.getHashedSubPackets().isPrimaryUserID()) {
292                if (currentModificationDate == null || creationTime.after(currentModificationDate)) {
293                    primaryUserId = userId;
294                    currentModificationDate = creationTime;
295                }
296
297            }
298        }
299
300        if (primaryUserId != null) {
301            return primaryUserId;
302        }
303
304        return firstUserId;
305    }
306
307    /**
308     * Return a list of all user-ids of the primary key.
309     * Note: This list might also contain expired / revoked user-ids.
310     * Consider using {@link #getValidUserIds()} instead.
311     *
312     * @return list of user-ids
313     */
314    public List<String> getUserIds() {
315        Iterator<String> iterator = getPublicKey().getUserIDs();
316        List<String> userIds = iteratorToList(iterator);
317        return userIds;
318    }
319
320    /**
321     * Return a list of valid user-ids.
322     *
323     * @return valid user-ids
324     */
325    public List<String> getValidUserIds() {
326        List<String> valid = new ArrayList<>();
327        List<String> userIds = getUserIds();
328        for (String userId : userIds) {
329            if (isUserIdBound(userId)) {
330                valid.add(userId);
331            }
332        }
333        return valid;
334    }
335
336    /**
337     * Return a list of all user-ids that were valid at some point, but might be expired by now.
338     *
339     * @return bound user-ids
340     */
341    public List<String> getValidAndExpiredUserIds() {
342        List<String> probablyExpired = new ArrayList<>();
343        List<String> userIds = getUserIds();
344
345        for (String userId : userIds) {
346            PGPSignature certification = signatures.userIdCertifications.get(userId);
347            PGPSignature revocation = signatures.userIdRevocations.get(userId);
348
349            // Not revoked -> valid
350            if (revocation == null) {
351                probablyExpired.add(userId);
352                continue;
353            }
354
355            // Hard revocation -> invalid
356            if (SignatureUtils.isHardRevocation(revocation)) {
357                continue;
358            }
359
360            // Soft revocation -> valid if certification is newer than revocation (revalidation)
361            if (certification.getCreationTime().after(revocation.getCreationTime())) {
362                probablyExpired.add(userId);
363            }
364        }
365        return probablyExpired;
366    }
367
368    /**
369     * Return true if the provided user-id is valid.
370     *
371     * @param userId user-id
372     * @return true if user-id is valid
373     */
374    public boolean isUserIdValid(String userId) {
375        if (!userId.equals(primaryUserId)) {
376            if (!isUserIdBound(primaryUserId)) {
377                // primary user-id not valid
378                return false;
379            }
380        }
381        return isUserIdBound(userId);
382    }
383
384
385    private boolean isUserIdBound(String userId) {
386
387        PGPSignature certification = signatures.userIdCertifications.get(userId);
388        PGPSignature revocation = signatures.userIdRevocations.get(userId);
389
390        if (certification == null) {
391            return false;
392        }
393        if (SignatureUtils.isSignatureExpired(certification)) {
394            return false;
395        }
396        if (certification.getHashedSubPackets().isPrimaryUserID()) {
397            Date keyExpiration = SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(certification, keys.getPublicKey());
398            if (keyExpiration != null && evaluationDate.after(keyExpiration)) {
399                return false;
400            }
401        }
402        // Not revoked -> valid
403        if (revocation == null) {
404            return true;
405        }
406        // Hard revocation -> invalid
407        if (SignatureUtils.isHardRevocation(revocation)) {
408            return false;
409        }
410        // Soft revocation -> valid if certification is newer than revocation (revalidation)
411        return certification.getCreationTime().after(revocation.getCreationTime());
412    }
413
414    /**
415     * Return a list of all user-ids of the primary key that appear to be email-addresses.
416     * Note: This list might contain expired / revoked user-ids.
417     *
418     * @return email addresses
419     */
420    public List<String> getEmailAddresses() {
421        List<String> userIds = getUserIds();
422        List<String> emails = new ArrayList<>();
423        for (String userId : userIds) {
424            Matcher matcher = PATTERN_EMAIL.matcher(userId);
425            if (matcher.find()) {
426                emails.add(matcher.group());
427            }
428        }
429        return emails;
430    }
431
432    /**
433     * Return the latest direct-key self signature.
434     *
435     * Note: This signature might be expired (check with {@link SignatureUtils#isSignatureExpired(PGPSignature)}).
436     *
437     * @return latest direct key self-signature or null
438     */
439    public @Nullable PGPSignature getLatestDirectKeySelfSignature() {
440        return signatures.primaryKeySelfSignature;
441    }
442
443    /**
444     * Return the latest revocation self-signature on the primary key.
445     *
446     * @return revocation or null
447     */
448    public @Nullable PGPSignature getRevocationSelfSignature() {
449        return signatures.primaryKeyRevocation;
450    }
451
452    /**
453     * Return the latest certification self-signature on the provided user-id.
454     *
455     * @param userId user-id
456     * @return certification signature or null
457     */
458    public @Nullable PGPSignature getLatestUserIdCertification(String userId) {
459        return signatures.userIdCertifications.get(userId);
460    }
461
462    /**
463     * Return the latest user-id revocation signature for the provided user-id.
464     *
465     * @param userId user-id
466     * @return revocation or null
467     */
468    public @Nullable PGPSignature getUserIdRevocation(String userId) {
469        return signatures.userIdRevocations.get(userId);
470    }
471
472    /**
473     * Return the currently active subkey binding signature for the subkey with the provided key-id.
474     *
475     * @param keyId subkey id
476     * @return subkey binding signature or null
477     */
478    public @Nullable PGPSignature getCurrentSubkeyBindingSignature(long keyId) {
479        return signatures.subkeyBindings.get(keyId);
480    }
481
482    /**
483     * Return the latest subkey binding revocation signature for the subkey with the given key-id.
484     *
485     * @param keyId subkey id
486     * @return subkey binding revocation or null
487     */
488    public @Nullable PGPSignature getSubkeyRevocationSignature(long keyId) {
489        return signatures.subkeyRevocations.get(keyId);
490    }
491
492    /**
493     * Return a list of {@link KeyFlag KeyFlags} that apply to the subkey with the provided key id.
494     * @param keyId key-id
495     * @return list of key flags
496     */
497    public @Nonnull List<KeyFlag> getKeyFlagsOf(long keyId) {
498        // key is primary key
499        if (getPublicKey().getKeyID() == keyId) {
500
501            PGPSignature directKeySignature = getLatestDirectKeySelfSignature();
502            if (directKeySignature != null) {
503                List<KeyFlag> keyFlags = SignatureSubpacketsUtil.parseKeyFlags(directKeySignature);
504                if (keyFlags != null) {
505                    return keyFlags;
506                }
507            }
508
509            String primaryUserId = getPrimaryUserId();
510            if (primaryUserId != null) {
511                PGPSignature userIdSignature = getLatestUserIdCertification(primaryUserId);
512                List<KeyFlag> keyFlags = SignatureSubpacketsUtil.parseKeyFlags(userIdSignature);
513                if (keyFlags != null) {
514                    return keyFlags;
515                }
516            }
517        }
518        // Key is subkey
519        else {
520            PGPSignature bindingSignature = getCurrentSubkeyBindingSignature(keyId);
521            if (bindingSignature != null) {
522                List<KeyFlag> keyFlags = SignatureSubpacketsUtil.parseKeyFlags(bindingSignature);
523                if (keyFlags != null) {
524                    return keyFlags;
525                }
526            }
527        }
528        return Collections.emptyList();
529    }
530
531    /**
532     * Return a list of {@link KeyFlag KeyFlags} that apply to the given user-id.
533     *
534     * @param userId user-id
535     * @return key flags
536     */
537    public @Nonnull List<KeyFlag> getKeyFlagsOf(String userId) {
538        if (!isUserIdValid(userId)) {
539            return Collections.emptyList();
540        }
541
542        PGPSignature userIdCertification = getLatestUserIdCertification(userId);
543        if (userIdCertification == null) {
544            throw new AssertionError("While user-id '" + userId + "' was reported as valid, there appears to be no certification for it.");
545        }
546
547        List<KeyFlag> keyFlags = SignatureSubpacketsUtil.parseKeyFlags(userIdCertification);
548        if (keyFlags != null) {
549            return keyFlags;
550        }
551        return Collections.emptyList();
552    }
553
554    /**
555     * Return the algorithm of the primary key.
556     *
557     * @return public key algorithm
558     */
559    public PublicKeyAlgorithm getAlgorithm() {
560        return PublicKeyAlgorithm.fromId(getPublicKey().getAlgorithm());
561    }
562
563    /**
564     * Return the creation date of the primary key.
565     *
566     * @return creation date
567     */
568    public Date getCreationDate() {
569        return getPublicKey().getCreationTime();
570    }
571
572    /**
573     * Return the date on which the key ring was last modified.
574     * This date corresponds to the date of the last signature that was made on this key ring by the primary key.
575     *
576     * @return last modification date.
577     */
578    public @Nullable Date getLastModified() {
579        PGPSignature mostRecent = getMostRecentSignature();
580        if (mostRecent == null) {
581            // No sigs found. Return public key creation date instead.
582            return getLatestKeyCreationDate();
583        }
584        return mostRecent.getCreationTime();
585    }
586
587    /**
588     * Return the creation time of the latest added subkey.
589     *
590     * @return latest key creation time
591     */
592    public @Nonnull Date getLatestKeyCreationDate() {
593        Date latestCreation = null;
594        for (PGPPublicKey key : getPublicKeys()) {
595            if (!isKeyValidlyBound(key.getKeyID())) {
596                continue;
597            }
598            Date keyCreation = key.getCreationTime();
599            if (latestCreation == null || latestCreation.before(keyCreation)) {
600                latestCreation = keyCreation;
601            }
602        }
603        if (latestCreation == null) {
604            throw new AssertionError("Apparently there is no validly bound key in this key ring.");
605        }
606        return latestCreation;
607    }
608
609    private @Nullable PGPSignature getMostRecentSignature() {
610        Set<PGPSignature> allSignatures = new HashSet<>();
611        PGPSignature mostRecentSelfSignature = getLatestDirectKeySelfSignature();
612        PGPSignature revocationSelfSignature = getRevocationSelfSignature();
613        if (mostRecentSelfSignature != null) allSignatures.add(mostRecentSelfSignature);
614        if (revocationSelfSignature != null) allSignatures.add(revocationSelfSignature);
615        allSignatures.addAll(signatures.userIdCertifications.values());
616        allSignatures.addAll(signatures.userIdRevocations.values());
617        allSignatures.addAll(signatures.subkeyBindings.values());
618        allSignatures.addAll(signatures.subkeyRevocations.values());
619
620        PGPSignature mostRecent = null;
621        for (PGPSignature signature : allSignatures) {
622            if (mostRecent == null || signature.getCreationTime().after(mostRecent.getCreationTime())) {
623                mostRecent = signature;
624            }
625        }
626        return mostRecent;
627    }
628
629    /**
630     * Return the date on which the primary key was revoked, or null if it has not yet been revoked.
631     *
632     * @return revocation date or null
633     */
634    public @Nullable Date getRevocationDate() {
635        return getRevocationSelfSignature() == null ? null : getRevocationSelfSignature().getCreationTime();
636    }
637
638    /**
639     * Return the date of expiration of the primary key or null if the key has no expiration date.
640     *
641     * @return expiration date
642     */
643    public @Nullable Date getPrimaryKeyExpirationDate() {
644        PGPSignature directKeySig = getLatestDirectKeySelfSignature();
645        Date directKeyExpirationDate = null;
646        if (directKeySig != null) {
647            directKeyExpirationDate = SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(directKeySig, getPublicKey());
648        }
649
650        PGPSignature primaryUserIdCertification = null;
651        Date userIdExpirationDate = null;
652        String possiblyExpiredPrimaryUserId = getPossiblyExpiredPrimaryUserId();
653        if (possiblyExpiredPrimaryUserId != null) {
654            primaryUserIdCertification = getLatestUserIdCertification(possiblyExpiredPrimaryUserId);
655            if (primaryUserIdCertification != null) {
656                userIdExpirationDate = SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(primaryUserIdCertification, getPublicKey());
657            }
658        }
659
660        if (directKeySig == null && primaryUserIdCertification == null) {
661            throw new NoSuchElementException("No direct-key signature and no user-id signature found.");
662        }
663
664        if (directKeyExpirationDate != null && userIdExpirationDate == null) {
665            return directKeyExpirationDate;
666        }
667
668        if (directKeyExpirationDate == null) {
669            return userIdExpirationDate;
670        }
671
672        if (directKeyExpirationDate.before(userIdExpirationDate)) {
673            return directKeyExpirationDate;
674        }
675
676        return userIdExpirationDate;
677    }
678
679    public String getPossiblyExpiredPrimaryUserId() {
680        String validPrimaryUserId = getPrimaryUserId();
681        if (validPrimaryUserId != null) {
682            return validPrimaryUserId;
683        }
684
685        Date latestCreationTime = null;
686        String primaryUserId = null;
687        boolean foundPrimary = false;
688        for (String userId : getUserIds()) {
689            PGPSignature signature = getLatestUserIdCertification(userId);
690            if (signature == null) {
691                continue;
692            }
693
694            boolean isPrimary = signature.getHashedSubPackets().isPrimaryUserID();
695            if (foundPrimary && !isPrimary) {
696                continue;
697            }
698
699            Date creationTime = signature.getCreationTime();
700            if (latestCreationTime == null || creationTime.after(latestCreationTime) || isPrimary && !foundPrimary) {
701                latestCreationTime = creationTime;
702                primaryUserId = userId;
703            }
704
705            foundPrimary |= isPrimary;
706        }
707
708        return primaryUserId;
709    }
710
711    /**
712     * Return the expiration date of the subkey with the provided fingerprint.
713     *
714     * @param fingerprint subkey fingerprint
715     * @return expiration date or null
716     */
717    public @Nullable Date getSubkeyExpirationDate(OpenPgpFingerprint fingerprint) {
718        if (getPublicKey().getKeyID() == fingerprint.getKeyId()) {
719            return getPrimaryKeyExpirationDate();
720        }
721
722        PGPPublicKey subkey = getPublicKey(fingerprint.getKeyId());
723        if (subkey == null) {
724            throw new NoSuchElementException("No subkey with fingerprint " + fingerprint + " found.");
725        }
726
727        PGPSignature bindingSig = getCurrentSubkeyBindingSignature(fingerprint.getKeyId());
728        if (bindingSig == null) {
729            throw new AssertionError("Subkey has no valid binding signature.");
730        }
731
732        return SignatureUtils.getKeyExpirationDate(subkey.getCreationTime(), bindingSig);
733    }
734
735    /**
736     * Return the latest date on which  the key ring is still usable for the given key flag.
737     * If only a subkey is carrying the required flag and the primary key expires earlier than the subkey,
738     * the expiry date of the primary key is returned.
739     *
740     * This method might return null, if the primary key and a subkey with the required flag does not expire.
741     * @param use key flag representing the use case, e.g. {@link KeyFlag#SIGN_DATA} or
742     * {@link KeyFlag#ENCRYPT_COMMS}/{@link KeyFlag#ENCRYPT_STORAGE}.
743     * @return latest date on which the key ring can be used for the given use case, or null if it can be used indefinitely.
744     */
745    public Date getExpirationDateForUse(KeyFlag use) {
746        if (use == KeyFlag.SPLIT || use == KeyFlag.SHARED) {
747            throw new IllegalArgumentException("SPLIT and SHARED are not uses, but properties.");
748        }
749
750        Date primaryExpiration = getPrimaryKeyExpirationDate();
751        List<PGPPublicKey> nonExpiringSubkeys = new ArrayList<>();
752        Date latestSubkeyExpirationDate = null;
753
754        List<PGPPublicKey> keysWithFlag = getKeysWithKeyFlag(use);
755        if (keysWithFlag.isEmpty()) {
756            throw new NoSuchElementException("No key with the required key flag found.");
757        }
758
759        for (PGPPublicKey key : keysWithFlag) {
760            Date subkeyExpirationDate = getSubkeyExpirationDate(OpenPgpFingerprint.of(key));
761            if (subkeyExpirationDate == null) {
762                nonExpiringSubkeys.add(key);
763            } else {
764                if (latestSubkeyExpirationDate == null || subkeyExpirationDate.after(latestSubkeyExpirationDate)) {
765                    latestSubkeyExpirationDate = subkeyExpirationDate;
766                }
767            }
768        }
769
770        if (nonExpiringSubkeys.isEmpty()) {
771            if (latestSubkeyExpirationDate != null) {
772                if (primaryExpiration == null) {
773                    return latestSubkeyExpirationDate;
774                }
775                if (latestSubkeyExpirationDate.before(primaryExpiration)) {
776                    return latestSubkeyExpirationDate;
777                }
778            }
779        }
780        return primaryExpiration;
781    }
782
783    public boolean isHardRevoked(String userId) {
784        PGPSignature revocation = signatures.userIdRevocations.get(userId);
785        if (revocation == null) {
786            return false;
787        }
788        RevocationReason revocationReason = revocation.getHashedSubPackets().getRevocationReason();
789        return revocationReason == null || RevocationAttributes.Reason.isHardRevocation(revocationReason.getRevocationReason());
790    }
791
792    /**
793     * Return true if the key ring is a {@link PGPSecretKeyRing}.
794     * If it is a {@link PGPPublicKeyRing} return false and if it is neither, throw an {@link AssertionError}.
795     *
796     * @return true if the key ring is a secret key ring.
797     */
798    public boolean isSecretKey() {
799        if (keys instanceof PGPSecretKeyRing) {
800            return true;
801        } else if (keys instanceof PGPPublicKeyRing) {
802            return false;
803        } else {
804            throw new AssertionError("Expected PGPKeyRing to be either PGPPublicKeyRing or PGPSecretKeyRing, but got " + keys.getClass().getName() + " instead.");
805        }
806    }
807
808    /**
809     * Returns true when every secret key on the key ring is not encrypted.
810     * If there is at least one encrypted secret key on the key ring, returns false.
811     * If the key ring is a {@link PGPPublicKeyRing}, returns true.
812     * Sub-keys with S2K of a type GNU_DUMMY_S2K do not affect the result.
813     *
814     * @return true if all secret keys are unencrypted.
815     */
816    public boolean isFullyDecrypted() {
817        if (!isSecretKey()) {
818            return true;
819        }
820        for (PGPSecretKey secretKey : getSecretKeys()) {
821            if (!KeyInfo.hasDummyS2K(secretKey) && KeyInfo.isEncrypted(secretKey)) {
822                return false;
823            }
824        }
825        return true;
826    }
827
828    /**
829     * Returns true when every secret key on the key ring is encrypted.
830     * If there is at least one not encrypted secret key on the key ring, returns false.
831     * If the key ring is a {@link PGPPublicKeyRing}, returns false.
832     * Sub-keys with S2K of a type GNU_DUMMY_S2K do not affect a result.
833     *
834     * @return true if all secret keys are encrypted.
835     */
836    public boolean isFullyEncrypted() {
837        if (!isSecretKey()) {
838            return false;
839        }
840        for (PGPSecretKey secretKey : getSecretKeys()) {
841            if (!KeyInfo.hasDummyS2K(secretKey) && KeyInfo.isDecrypted(secretKey)) {
842                return false;
843            }
844        }
845        return true;
846    }
847
848    /**
849     * Return the version number of the public keys format.
850     *
851     * @return version
852     */
853    public int getVersion() {
854        return keys.getPublicKey().getVersion();
855    }
856
857    /**
858     * Return a list of all subkeys which can be used for encryption of the given purpose.
859     * This list does not include expired or revoked keys.
860     *
861     * @param purpose purpose (encrypt data at rest / communications)
862     * @return encryption subkeys
863     */
864    public @Nonnull List<PGPPublicKey> getEncryptionSubkeys(EncryptionPurpose purpose) {
865        Date primaryExpiration = getPrimaryKeyExpirationDate();
866        if (primaryExpiration != null && primaryExpiration.before(new Date())) {
867            return Collections.emptyList();
868        }
869
870        Iterator<PGPPublicKey> subkeys = keys.getPublicKeys();
871        List<PGPPublicKey> encryptionKeys = new ArrayList<>();
872        while (subkeys.hasNext()) {
873            PGPPublicKey subKey = subkeys.next();
874
875            if (!isKeyValidlyBound(subKey.getKeyID())) {
876                continue;
877            }
878
879            Date subkeyExpiration = getSubkeyExpirationDate(OpenPgpFingerprint.of(subKey));
880            if (subkeyExpiration != null && subkeyExpiration.before(new Date())) {
881                continue;
882            }
883
884            if (!subKey.isEncryptionKey()) {
885                continue;
886            }
887
888            List<KeyFlag> keyFlags = getKeyFlagsOf(subKey.getKeyID());
889            switch (purpose) {
890                case COMMUNICATIONS:
891                    if (keyFlags.contains(KeyFlag.ENCRYPT_COMMS)) {
892                        encryptionKeys.add(subKey);
893                    }
894                    break;
895                case STORAGE:
896                    if (keyFlags.contains(KeyFlag.ENCRYPT_STORAGE)) {
897                        encryptionKeys.add(subKey);
898                    }
899                    break;
900                case ANY:
901                    if (keyFlags.contains(KeyFlag.ENCRYPT_COMMS) || keyFlags.contains(KeyFlag.ENCRYPT_STORAGE)) {
902                        encryptionKeys.add(subKey);
903                    }
904                    break;
905            }
906        }
907        return encryptionKeys;
908    }
909
910    /**
911     * Return a list of all keys which carry the provided key flag in their signature.
912     *
913     * @param flag flag
914     * @return keys with flag
915     */
916    public List<PGPPublicKey> getKeysWithKeyFlag(KeyFlag flag) {
917        List<PGPPublicKey> keysWithFlag = new ArrayList<>();
918        for (PGPPublicKey key : getPublicKeys()) {
919            List<KeyFlag> keyFlags = getKeyFlagsOf(key.getKeyID());
920            if (keyFlags.contains(flag)) {
921                keysWithFlag.add(key);
922            }
923        }
924
925        return keysWithFlag;
926    }
927
928    /**
929     * Return a list of all subkeys that can be used for encryption with the given user-id.
930     * This list does not include expired or revoked keys.
931     * TODO: Does it make sense to pass in a user-id?
932     *   Aren't the encryption subkeys the same, regardless of which user-id is used?
933     *
934     * @param userId user-id
935     * @param purpose encryption purpose
936     * @return encryption subkeys
937     */
938    public @Nonnull List<PGPPublicKey> getEncryptionSubkeys(String userId, EncryptionPurpose purpose) {
939        if (userId != null && !isUserIdValid(userId)) {
940            throw new KeyValidationError(userId, getLatestUserIdCertification(userId), getUserIdRevocation(userId));
941        }
942
943        return getEncryptionSubkeys(purpose);
944    }
945
946    /**
947     * Return a list of all subkeys which can be used to sign data.
948     *
949     * @return signing keys
950     */
951    public @Nonnull List<PGPPublicKey> getSigningSubkeys() {
952        Iterator<PGPPublicKey> subkeys = keys.getPublicKeys();
953        List<PGPPublicKey> signingKeys = new ArrayList<>();
954        while (subkeys.hasNext()) {
955            PGPPublicKey subKey = subkeys.next();
956
957            if (!isKeyValidlyBound(subKey.getKeyID())) {
958                continue;
959            }
960
961            List<KeyFlag> keyFlags = getKeyFlagsOf(subKey.getKeyID());
962            if (keyFlags.contains(KeyFlag.SIGN_DATA)) {
963                signingKeys.add(subKey);
964            }
965        }
966        return signingKeys;
967    }
968
969    public Set<HashAlgorithm> getPreferredHashAlgorithms() {
970        return getPreferredHashAlgorithms(getPrimaryUserId());
971    }
972
973    public Set<HashAlgorithm> getPreferredHashAlgorithms(String userId) {
974        return getKeyAccessor(userId, getKeyId()).getPreferredHashAlgorithms();
975    }
976
977    public Set<HashAlgorithm> getPreferredHashAlgorithms(long keyId) {
978        return getKeyAccessor(null, keyId).getPreferredHashAlgorithms();
979    }
980
981    public Set<SymmetricKeyAlgorithm> getPreferredSymmetricKeyAlgorithms() {
982        return getPreferredSymmetricKeyAlgorithms(getPrimaryUserId());
983    }
984
985    public Set<SymmetricKeyAlgorithm> getPreferredSymmetricKeyAlgorithms(String userId) {
986        return getKeyAccessor(userId, getKeyId()).getPreferredSymmetricKeyAlgorithms();
987    }
988
989    public Set<SymmetricKeyAlgorithm> getPreferredSymmetricKeyAlgorithms(long keyId) {
990        return getKeyAccessor(null, keyId).getPreferredSymmetricKeyAlgorithms();
991    }
992
993    public Set<CompressionAlgorithm> getPreferredCompressionAlgorithms() {
994        return getPreferredCompressionAlgorithms(getPrimaryUserId());
995    }
996
997    public Set<CompressionAlgorithm> getPreferredCompressionAlgorithms(String userId) {
998        return getKeyAccessor(userId, getKeyId()).getPreferredCompressionAlgorithms();
999    }
1000
1001    public Set<CompressionAlgorithm> getPreferredCompressionAlgorithms(long keyId) {
1002        return getKeyAccessor(null, keyId).getPreferredCompressionAlgorithms();
1003    }
1004
1005    private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) {
1006        if (getPublicKey(keyID) == null) {
1007            throw new IllegalArgumentException("No subkey with key id " + Long.toHexString(keyID) + " found on this key.");
1008        }
1009        if (userId != null && !getUserIds().contains(userId)) {
1010            throw new IllegalArgumentException("No user-id '" + userId + "' found on this key.");
1011        }
1012        return userId == null ? new KeyAccessor.ViaKeyId(this, new SubkeyIdentifier(keys, keyID))
1013                : new KeyAccessor.ViaUserId(this, new SubkeyIdentifier(keys, keyID), userId);
1014    }
1015
1016    public static class Signatures {
1017
1018        private final PGPSignature primaryKeyRevocation;
1019        private final PGPSignature primaryKeySelfSignature;
1020        private final Map<String, PGPSignature> userIdRevocations;
1021        private final Map<String, PGPSignature> userIdCertifications;
1022        private final Map<Long, PGPSignature> subkeyRevocations;
1023        private final Map<Long, PGPSignature> subkeyBindings;
1024
1025        public Signatures(PGPKeyRing keyRing, Date evaluationDate, Policy policy) {
1026            primaryKeyRevocation = SignaturePicker.pickCurrentRevocationSelfSignature(keyRing, policy, evaluationDate);
1027            primaryKeySelfSignature = SignaturePicker.pickLatestDirectKeySignature(keyRing, policy, evaluationDate);
1028            userIdRevocations = new HashMap<>();
1029            userIdCertifications = new HashMap<>();
1030            subkeyRevocations = new HashMap<>();
1031            subkeyBindings = new HashMap<>();
1032
1033            for (Iterator<String> it = keyRing.getPublicKey().getUserIDs(); it.hasNext(); ) {
1034                String userId = it.next();
1035                PGPSignature revocation = SignaturePicker.pickCurrentUserIdRevocationSignature(keyRing, userId, policy, evaluationDate);
1036                if (revocation != null) {
1037                    userIdRevocations.put(userId, revocation);
1038                }
1039                PGPSignature certification = SignaturePicker.pickLatestUserIdCertificationSignature(keyRing, userId, policy, evaluationDate);
1040                if (certification != null) {
1041                    userIdCertifications.put(userId, certification);
1042                }
1043            }
1044
1045            Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
1046            keys.next(); // Skip primary key
1047            while (keys.hasNext()) {
1048                PGPPublicKey subkey = keys.next();
1049                PGPSignature subkeyRevocation = SignaturePicker.pickCurrentSubkeyBindingRevocationSignature(keyRing, subkey, policy, evaluationDate);
1050                if (subkeyRevocation != null) {
1051                    subkeyRevocations.put(subkey.getKeyID(), subkeyRevocation);
1052                }
1053                PGPSignature subkeyBinding = SignaturePicker.pickLatestSubkeyBindingSignature(keyRing, subkey, policy, evaluationDate);
1054                if (subkeyBinding != null) {
1055                    subkeyBindings.put(subkey.getKeyID(), subkeyBinding);
1056                }
1057            }
1058        }
1059    }
1060}