001// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.decryption_verification;
006
007import java.util.ArrayList;
008import java.util.Collections;
009import java.util.Date;
010import java.util.HashSet;
011import java.util.List;
012import java.util.Map;
013import java.util.Set;
014import java.util.concurrent.ConcurrentHashMap;
015import javax.annotation.Nonnull;
016import javax.annotation.Nullable;
017
018import org.bouncycastle.openpgp.PGPLiteralData;
019import org.bouncycastle.openpgp.PGPPublicKey;
020import org.bouncycastle.openpgp.PGPPublicKeyRing;
021import org.bouncycastle.openpgp.PGPSignature;
022import org.pgpainless.algorithm.CompressionAlgorithm;
023import org.pgpainless.algorithm.StreamEncoding;
024import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
025import org.pgpainless.exception.SignatureValidationException;
026import org.pgpainless.key.OpenPgpFingerprint;
027import org.pgpainless.key.SubkeyIdentifier;
028import org.pgpainless.util.SessionKey;
029
030public class OpenPgpMetadata {
031
032    private final Set<Long> recipientKeyIds;
033    private final SubkeyIdentifier decryptionKey;
034    private final List<SignatureVerification> verifiedInbandSignatures;
035    private final List<SignatureVerification.Failure> invalidInbandSignatures;
036    private final List<SignatureVerification> verifiedDetachedSignatures;
037    private final List<SignatureVerification.Failure> invalidDetachedSignatures;
038    private final SessionKey sessionKey;
039    private final CompressionAlgorithm compressionAlgorithm;
040    private final String fileName;
041    private final Date modificationDate;
042    private final StreamEncoding fileEncoding;
043
044    public OpenPgpMetadata(Set<Long> recipientKeyIds,
045                           SubkeyIdentifier decryptionKey,
046                           SessionKey sessionKey,
047                           CompressionAlgorithm algorithm,
048                           List<SignatureVerification> verifiedInbandSignatures,
049                           List<SignatureVerification.Failure> invalidInbandSignatures,
050                           List<SignatureVerification> verifiedDetachedSignatures,
051                           List<SignatureVerification.Failure> invalidDetachedSignatures,
052                           String fileName,
053                           Date modificationDate,
054                           StreamEncoding fileEncoding) {
055
056        this.recipientKeyIds = Collections.unmodifiableSet(recipientKeyIds);
057        this.decryptionKey = decryptionKey;
058        this.sessionKey = sessionKey;
059        this.compressionAlgorithm = algorithm;
060        this.verifiedInbandSignatures = Collections.unmodifiableList(verifiedInbandSignatures);
061        this.invalidInbandSignatures = Collections.unmodifiableList(invalidInbandSignatures);
062        this.verifiedDetachedSignatures = Collections.unmodifiableList(verifiedDetachedSignatures);
063        this.invalidDetachedSignatures = Collections.unmodifiableList(invalidDetachedSignatures);
064        this.fileName = fileName;
065        this.modificationDate = modificationDate;
066        this.fileEncoding = fileEncoding;
067    }
068
069    /**
070     * Return a set of key-ids the messages was encrypted for.
071     *
072     * @return recipient ids
073     */
074    public @Nonnull Set<Long> getRecipientKeyIds() {
075        return recipientKeyIds;
076    }
077
078    /**
079     * Return true, if the message was encrypted.
080     *
081     * @return true if encrypted, false otherwise
082     */
083    public boolean isEncrypted() {
084        return sessionKey != null && sessionKey.getAlgorithm() != SymmetricKeyAlgorithm.NULL && !getRecipientKeyIds().isEmpty();
085    }
086
087    /**
088     * Return the {@link SubkeyIdentifier} of the key that was used to decrypt the message.
089     * This can be null if the message was decrypted using a {@link org.pgpainless.util.Passphrase}, or if it was not
090     * encrypted at all (e.g. signed only).
091     *
092     * @return subkey identifier of decryption key
093     */
094    public @Nullable SubkeyIdentifier getDecryptionKey() {
095        return decryptionKey;
096    }
097
098    /**
099     * Return the algorithm that was used to symmetrically encrypt the message.
100     *
101     * @return encryption algorithm
102     */
103    public @Nullable SymmetricKeyAlgorithm getSymmetricKeyAlgorithm() {
104        return sessionKey == null ? null : sessionKey.getAlgorithm();
105    }
106
107    public @Nullable SessionKey getSessionKey() {
108        return sessionKey;
109    }
110
111    /**
112     * Return the {@link CompressionAlgorithm} that was used to compress the message.
113     *
114     * @return compression algorithm
115     */
116    public @Nullable CompressionAlgorithm getCompressionAlgorithm() {
117        return compressionAlgorithm;
118    }
119
120    /**
121     * Return a set of all signatures on the message.
122     * Note: This method returns just the signatures. There is no guarantee that the signatures are verified or even correct.
123     *
124     * Use {@link #getVerifiedSignatures()} instead to get all verified signatures.
125     * @return unverified and verified signatures
126     */
127    public @Nonnull Set<PGPSignature> getSignatures() {
128        Set<PGPSignature> signatures = new HashSet<>();
129        for (SignatureVerification v : getVerifiedDetachedSignatures()) {
130            signatures.add(v.getSignature());
131        }
132        for (SignatureVerification v : getVerifiedInbandSignatures()) {
133            signatures.add(v.getSignature());
134        }
135        for (SignatureVerification.Failure f : getInvalidDetachedSignatures()) {
136            signatures.add(f.getSignatureVerification().getSignature());
137        }
138        for (SignatureVerification.Failure f : getInvalidInbandSignatures()) {
139            signatures.add(f.getSignatureVerification().getSignature());
140        }
141        return signatures;
142    }
143
144    /**
145     * Return true if the message contained at least one signature.
146     *
147     * Note: This method does not reflect, whether the signature on the message is correct.
148     * Use {@link #isVerified()} instead to determine, if the message carries a verifiable signature.
149     *
150     * @return true if message contains at least one unverified or verified signature, false otherwise.
151     */
152    public boolean isSigned() {
153        return !getSignatures().isEmpty();
154    }
155
156    /**
157     * Return a map of all verified signatures on the message.
158     * The map contains verified signatures as value, with the {@link SubkeyIdentifier} of the key that was used to verify
159     * the signature as the maps keys.
160     *
161     * @return verified detached and one-pass signatures
162     */
163    public Map<SubkeyIdentifier, PGPSignature> getVerifiedSignatures() {
164        Map<SubkeyIdentifier, PGPSignature> verifiedSignatures = new ConcurrentHashMap<>();
165        for (SignatureVerification detachedSignature : getVerifiedDetachedSignatures()) {
166            verifiedSignatures.put(detachedSignature.getSigningKey(), detachedSignature.getSignature());
167        }
168        for (SignatureVerification inbandSignatures : verifiedInbandSignatures) {
169            verifiedSignatures.put(inbandSignatures.getSigningKey(), inbandSignatures.getSignature());
170        }
171
172        return verifiedSignatures;
173    }
174
175    public List<SignatureVerification> getVerifiedInbandSignatures() {
176        return verifiedInbandSignatures;
177    }
178
179    public List<SignatureVerification> getVerifiedDetachedSignatures() {
180        return verifiedDetachedSignatures;
181    }
182
183    public List<SignatureVerification.Failure> getInvalidInbandSignatures() {
184        return invalidInbandSignatures;
185    }
186
187    public List<SignatureVerification.Failure> getInvalidDetachedSignatures() {
188        return invalidDetachedSignatures;
189    }
190
191    /**
192     * Return true, if the message is signed and at least one signature on the message was verified successfully.
193     *
194     * @return true if message is verified, false otherwise
195     */
196    public boolean isVerified() {
197        return !getVerifiedSignatures().isEmpty();
198    }
199
200    /**
201     * Return true, if the message contains at least one verified signature made by a key in the
202     * given certificate.
203     *
204     * @param certificate certificate
205     * @return true if message was signed by the certificate (and the signature is valid), false otherwise
206     */
207    public boolean containsVerifiedSignatureFrom(PGPPublicKeyRing certificate) {
208        for (PGPPublicKey key : certificate) {
209            OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(key);
210            if (containsVerifiedSignatureFrom(fingerprint)) {
211                return true;
212            }
213        }
214        return false;
215    }
216
217    /**
218     * Return true, if the message contains at least one valid signature made by the key with the given
219     * fingerprint, false otherwise.
220     *
221     * The fingerprint might be of the signing subkey, or the primary key of the signing certificate.
222     *
223     * @param fingerprint fingerprint of primary key or signing subkey
224     * @return true if validly signed, false otherwise
225     */
226    public boolean containsVerifiedSignatureFrom(OpenPgpFingerprint fingerprint) {
227        for (SubkeyIdentifier verifiedSigningKey : getVerifiedSignatures().keySet()) {
228            if (verifiedSigningKey.getPrimaryKeyFingerprint().equals(fingerprint) ||
229                    verifiedSigningKey.getSubkeyFingerprint().equals(fingerprint)) {
230                return true;
231            }
232        }
233        return false;
234    }
235
236    /**
237     * Return the name of the encrypted / signed file.
238     *
239     * @return file name
240     */
241    public String getFileName() {
242        return fileName;
243    }
244
245    /**
246     * Return true, if the encrypted data is intended for your eyes only.
247     *
248     * @return true if for-your-eyes-only
249     */
250    public boolean isForYourEyesOnly() {
251        return PGPLiteralData.CONSOLE.equals(getFileName());
252    }
253
254    /**
255     * Return the modification date of the encrypted / signed file.
256     *
257     * @return modification date
258     */
259    public Date getModificationDate() {
260        return modificationDate;
261    }
262
263    /**
264     * Return the encoding format of the encrypted / signed file.
265     *
266     * @return encoding
267     */
268    public StreamEncoding getFileEncoding() {
269        return fileEncoding;
270    }
271
272    public static Builder getBuilder() {
273        return new Builder();
274    }
275
276    public static class Builder {
277
278        private final Set<Long> recipientFingerprints = new HashSet<>();
279        private SessionKey sessionKey;
280        private SubkeyIdentifier decryptionKey;
281        private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED;
282        private String fileName;
283        private StreamEncoding fileEncoding;
284        private Date modificationDate;
285
286        private final List<SignatureVerification> verifiedInbandSignatures = new ArrayList<>();
287        private final List<SignatureVerification> verifiedDetachedSignatures = new ArrayList<>();
288        private final List<SignatureVerification.Failure> invalidInbandSignatures = new ArrayList<>();
289        private final List<SignatureVerification.Failure> invalidDetachedSignatures = new ArrayList<>();
290
291
292        public Builder addRecipientKeyId(Long keyId) {
293            this.recipientFingerprints.add(keyId);
294            return this;
295        }
296
297        public Builder setDecryptionKey(SubkeyIdentifier decryptionKey) {
298            this.decryptionKey = decryptionKey;
299            return this;
300        }
301
302        public Builder setSessionKey(SessionKey sessionKey) {
303            this.sessionKey = sessionKey;
304            return this;
305        }
306
307        public Builder setCompressionAlgorithm(CompressionAlgorithm algorithm) {
308            this.compressionAlgorithm = algorithm;
309            return this;
310        }
311
312        public Builder setFileName(@Nonnull String fileName) {
313            this.fileName = fileName;
314            return this;
315        }
316
317        public Builder setModificationDate(Date modificationDate) {
318            this.modificationDate = modificationDate;
319            return this;
320        }
321
322        public Builder setFileEncoding(StreamEncoding encoding) {
323            this.fileEncoding = encoding;
324            return this;
325        }
326
327        public OpenPgpMetadata build() {
328            return new OpenPgpMetadata(
329                    recipientFingerprints, decryptionKey,
330                    sessionKey, compressionAlgorithm,
331                    verifiedInbandSignatures, invalidInbandSignatures,
332                    verifiedDetachedSignatures, invalidDetachedSignatures,
333                    fileName, modificationDate, fileEncoding);
334        }
335
336        public void addVerifiedInbandSignature(SignatureVerification signatureVerification) {
337            this.verifiedInbandSignatures.add(signatureVerification);
338        }
339
340        public void addVerifiedDetachedSignature(SignatureVerification signatureVerification) {
341            this.verifiedDetachedSignatures.add(signatureVerification);
342        }
343
344        public void addInvalidInbandSignature(SignatureVerification signatureVerification, SignatureValidationException e) {
345            this.invalidInbandSignatures.add(new SignatureVerification.Failure(signatureVerification, e));
346        }
347
348        public void addInvalidDetachedSignature(SignatureVerification signatureVerification, SignatureValidationException e) {
349            this.invalidDetachedSignatures.add(new SignatureVerification.Failure(signatureVerification, e));
350        }
351    }
352}