001// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.key.util;
006
007import javax.annotation.Nonnull;
008import java.util.Map;
009import java.util.concurrent.ConcurrentHashMap;
010
011public final class RevocationAttributes {
012
013    /**
014     * Reason for revocation.
015     * There are two kinds of reasons: hard and soft reason.
016     *
017     * Soft revocation reasons gracefully disable keys or user-ids.
018     * Softly revoked keys can no longer be used to encrypt data to or to generate signatures.
019     * Any signature made after a key has been soft revoked is deemed invalid.
020     * Any signature made before the key has been soft revoked stays valid.
021     * Soft revoked info can be re-certified at a later point.
022     *
023     * Hard revocation reasons on the other hand renders the key or user-id invalid immediately.
024     * Hard reasons are suitable to use if for example a key got compromised.
025     * Any signature made before or after a key has been hard revoked is no longer considered valid.
026     * Hard revoked information can also not be re-certified.
027     */
028    public enum Reason {
029        /**
030         * The key or certification is being revoked without a reason.
031         * This is a HARD revocation reason and cannot be undone.
032         */
033        NO_REASON((byte) 0),
034        /**
035         * The key was superseded by another key.
036         * This is a SOFT revocation reason and can be undone.
037         */
038        KEY_SUPERSEDED((byte) 1),
039        /**
040         * The key has potentially been compromised.
041         * This is a HARD revocation reason and cannot be undone.
042         */
043        KEY_COMPROMISED((byte) 2),
044        /**
045         * The key was retired and shall no longer be used.
046         * This is a SOFT revocation reason can can be undone.
047         */
048        KEY_RETIRED((byte) 3),
049        /**
050         * The user-id is no longer valid.
051         * This is a SOFT revocation reason and can be undone.
052         */
053        USER_ID_NO_LONGER_VALID((byte) 32),
054        ;
055
056        private static final Map<Byte, Reason> MAP = new ConcurrentHashMap<>();
057        static {
058            for (Reason r : Reason.values()) {
059                MAP.put(r.reasonCode, r);
060            }
061        }
062
063        /**
064         * Decode a machine-readable reason code.
065         *
066         * @param code byte
067         * @return reason
068         */
069        public static Reason fromCode(byte code) {
070            Reason reason = MAP.get(code);
071            if (reason == null) {
072                throw new IllegalArgumentException("Invalid revocation reason: " + code);
073            }
074            return reason;
075        }
076
077        /**
078         * Return true if the {@link Reason} the provided code encodes is a hard revocation reason, false
079         * otherwise.
080         * Hard revocations cannot be undone, while keys or certifications with soft revocations can be
081         * re-certified by placing another signature on them.
082         *
083         * @param code reason code
084         * @return is hard
085         */
086        public static boolean isHardRevocation(byte code) {
087            Reason reason = MAP.get(code);
088            return reason != KEY_SUPERSEDED && reason != KEY_RETIRED && reason != USER_ID_NO_LONGER_VALID;
089        }
090
091        /**
092         * Return true if the given {@link Reason} is a hard revocation, false otherwise.
093         * Hard revocations cannot be undone, while keys or certifications with soft revocations can be
094         * re-certified by placing another signature on them.
095         *
096         * @param reason reason
097         * @return is hard
098         */
099        public static boolean isHardRevocation(@Nonnull Reason reason) {
100            return isHardRevocation(reason.reasonCode);
101        }
102
103        private final byte reasonCode;
104
105        Reason(byte reasonCode) {
106            this.reasonCode = reasonCode;
107        }
108
109        public byte code() {
110            return reasonCode;
111        }
112
113        @Override
114        public String toString() {
115            return code() + " - " + name();
116        }
117    }
118
119    public enum RevocationType {
120        KEY_REVOCATION,
121        CERT_REVOCATION
122    }
123
124    private final Reason reason;
125    private final String description;
126
127    private RevocationAttributes(Reason reason, String description) {
128        this.reason = reason;
129        this.description = description;
130    }
131
132    /**
133     * Return the machine-readable reason for revocation.
134     *
135     * @return reason
136     */
137    public @Nonnull Reason getReason() {
138        return reason;
139    }
140
141    /**
142     * Return the human-readable description for the revocation reason.
143     * @return description
144     */
145    public @Nonnull String getDescription() {
146        return description;
147    }
148
149    /**
150     * Build a {@link RevocationAttributes} object suitable for key revocations.
151     * Key revocations are revocations for keys or subkeys.
152     *
153     * @return builder
154     */
155    public static WithReason createKeyRevocation() {
156        return new WithReason(RevocationType.KEY_REVOCATION);
157    }
158
159    /**
160     * Build a {@link RevocationAttributes} object suitable for certification (e.g. user-id) revocations.
161     *
162     * @return builder
163     */
164    public static WithReason createCertificateRevocation() {
165        return new WithReason(RevocationType.CERT_REVOCATION);
166    }
167
168    public static final class WithReason {
169
170        private final RevocationType type;
171
172        private WithReason(RevocationType type) {
173            this.type = type;
174        }
175
176        /**
177         * Set the machine-readable reason.
178         * Note that depending on whether this is a key-revocation or certification-revocation,
179         * only certain reason codes are valid.
180         * Invalid input will result in an {@link IllegalArgumentException} to be thrown.
181         *
182         * @param reason reason
183         * @throws IllegalArgumentException in case of an invalid revocation reason
184         * @return builder
185         */
186        public WithDescription withReason(Reason reason) {
187            throwIfReasonTypeMismatch(reason, type);
188            return new WithDescription(reason);
189        }
190
191        private void throwIfReasonTypeMismatch(Reason reason, RevocationType type) {
192            if (type == RevocationType.KEY_REVOCATION) {
193                if (reason == Reason.USER_ID_NO_LONGER_VALID) {
194                    throw new IllegalArgumentException("Reason " + reason + " can only be used for certificate revocations, not to revoke keys.");
195                }
196            } else if (type == RevocationType.CERT_REVOCATION) {
197                switch (reason) {
198                    case KEY_SUPERSEDED:
199                    case KEY_COMPROMISED:
200                    case KEY_RETIRED:
201                        throw new IllegalArgumentException("Reason " + reason + " can only be used for key revocations, not to revoke certificates.");
202                }
203            }
204        }
205
206    }
207
208    public static final class WithDescription {
209
210        private final Reason reason;
211
212        private WithDescription(Reason reason) {
213            this.reason = reason;
214        }
215
216        /**
217         * Set a human-readable description of the revocation reason.
218         *
219         * @param description description
220         * @return revocation attributes
221         */
222        public RevocationAttributes withDescription(@Nonnull String description) {
223            return new RevocationAttributes(reason, description);
224        }
225
226        /**
227         * Set an empty human-readable description.
228         * @return revocation attributes
229         */
230        public RevocationAttributes withoutDescription() {
231            return withDescription("");
232        }
233    }
234}