001// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.util;
006
007import java.io.BufferedInputStream;
008import java.io.ByteArrayInputStream;
009import java.io.ByteArrayOutputStream;
010import java.io.IOException;
011import java.io.InputStream;
012import java.io.OutputStream;
013import java.util.ArrayList;
014import java.util.Iterator;
015import java.util.List;
016import java.util.regex.Pattern;
017
018import org.bouncycastle.bcpg.ArmoredInputStream;
019import org.bouncycastle.bcpg.ArmoredOutputStream;
020import org.bouncycastle.openpgp.PGPKeyRing;
021import org.bouncycastle.openpgp.PGPPublicKeyRing;
022import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
023import org.bouncycastle.openpgp.PGPSecretKeyRing;
024import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
025import org.bouncycastle.openpgp.PGPUtil;
026import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
027import org.bouncycastle.util.io.Streams;
028import org.pgpainless.algorithm.HashAlgorithm;
029import org.pgpainless.key.OpenPgpFingerprint;
030
031public final class ArmorUtils {
032
033    // MessageIDs are 32 printable characters
034    private static final Pattern PATTERN_MESSAGE_ID = Pattern.compile("^\\S{32}$");
035
036    public static final String HEADER_COMMENT = "Comment";
037    public static final String HEADER_VERSION = "Version";
038    public static final String HEADER_MESSAGEID = "MessageID";
039    public static final String HEADER_HASH = "Hash";
040    public static final String HEADER_CHARSET = "Charset";
041
042    private ArmorUtils() {
043
044    }
045
046    public static String toAsciiArmoredString(PGPSecretKeyRing secretKeys) throws IOException {
047        MultiMap<String, String> header = keyToHeader(secretKeys);
048        return toAsciiArmoredString(secretKeys.getEncoded(), header);
049    }
050
051    public static String toAsciiArmoredString(PGPPublicKeyRing publicKeys) throws IOException {
052        MultiMap<String, String> header = keyToHeader(publicKeys);
053        return toAsciiArmoredString(publicKeys.getEncoded(), header);
054    }
055
056    public static String toAsciiArmoredString(PGPSecretKeyRingCollection secretKeyRings) throws IOException {
057        StringBuilder sb = new StringBuilder();
058        for (Iterator<PGPSecretKeyRing> iterator = secretKeyRings.iterator(); iterator.hasNext(); ) {
059            PGPSecretKeyRing secretKeyRing = iterator.next();
060            sb.append(toAsciiArmoredString(secretKeyRing));
061            if (iterator.hasNext()) {
062                sb.append('\n');
063            }
064        }
065        return sb.toString();
066    }
067
068    public static ArmoredOutputStream toAsciiArmoredStream(PGPKeyRing keyRing, OutputStream outputStream) {
069        MultiMap<String, String> header = keyToHeader(keyRing);
070        return toAsciiArmoredStream(outputStream, header);
071    }
072
073    public static ArmoredOutputStream toAsciiArmoredStream(OutputStream outputStream, MultiMap<String, String> header) {
074        ArmoredOutputStream armoredOutputStream = ArmoredOutputStreamFactory.get(outputStream);
075        if (header != null) {
076            for (String headerKey : header.keySet()) {
077                for (String headerValue : header.get(headerKey)) {
078                    armoredOutputStream.addHeader(headerKey, headerValue);
079                }
080            }
081        }
082        return armoredOutputStream;
083    }
084
085    public static String toAsciiArmoredString(PGPPublicKeyRingCollection publicKeyRings) throws IOException {
086        StringBuilder sb = new StringBuilder();
087        for (Iterator<PGPPublicKeyRing> iterator = publicKeyRings.iterator(); iterator.hasNext(); ) {
088            PGPPublicKeyRing publicKeyRing = iterator.next();
089            sb.append(toAsciiArmoredString(publicKeyRing));
090            if (iterator.hasNext()) {
091                sb.append('\n');
092            }
093        }
094        return sb.toString();
095    }
096
097    private static MultiMap<String, String> keyToHeader(PGPKeyRing keyRing) {
098        MultiMap<String, String> header = new MultiMap<>();
099        OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(keyRing);
100        Iterator<String> userIds = keyRing.getPublicKey().getUserIDs();
101
102        header.put(HEADER_COMMENT, fingerprint.prettyPrint());
103        if (userIds.hasNext()) {
104            header.put(HEADER_COMMENT, userIds.next());
105        }
106        return header;
107    }
108
109    public static String toAsciiArmoredString(byte[] bytes) throws IOException {
110        return toAsciiArmoredString(bytes, null);
111    }
112
113    public static String toAsciiArmoredString(byte[] bytes, MultiMap<String, String> additionalHeaderValues) throws IOException {
114        return toAsciiArmoredString(new ByteArrayInputStream(bytes), additionalHeaderValues);
115    }
116
117    public static String toAsciiArmoredString(InputStream inputStream) throws IOException {
118        return toAsciiArmoredString(inputStream, null);
119    }
120
121    public static void addHashAlgorithmHeader(ArmoredOutputStream armor, HashAlgorithm hashAlgorithm) {
122        armor.addHeader(HEADER_HASH, hashAlgorithm.getAlgorithmName());
123    }
124
125    public static void addCommentHeader(ArmoredOutputStream armor, String comment) {
126        armor.addHeader(HEADER_COMMENT, comment);
127    }
128
129    public static void addMessageIdHeader(ArmoredOutputStream armor, String messageId) {
130        if (messageId == null) {
131            throw new NullPointerException("MessageID cannot be null.");
132        }
133        if (!PATTERN_MESSAGE_ID.matcher(messageId).matches()) {
134            throw new IllegalArgumentException("MessageIDs MUST consist of 32 printable characters.");
135        }
136        armor.addHeader(HEADER_MESSAGEID, messageId);
137    }
138
139    public static String toAsciiArmoredString(InputStream inputStream, MultiMap<String, String> additionalHeaderValues) throws IOException {
140        ByteArrayOutputStream out = new ByteArrayOutputStream();
141        ArmoredOutputStream armor = toAsciiArmoredStream(out, additionalHeaderValues);
142        Streams.pipeAll(inputStream, armor);
143        armor.close();
144
145        return out.toString();
146    }
147
148    public static ArmoredOutputStream createArmoredOutputStreamFor(PGPKeyRing keyRing, OutputStream outputStream) {
149        ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(outputStream);
150        MultiMap<String, String> headerMap = keyToHeader(keyRing);
151        for (String header : headerMap.keySet()) {
152            for (String value : headerMap.get(header)) {
153                armor.addHeader(header, value);
154            }
155        }
156
157        return armor;
158    }
159
160    public static List<String> getCommentHeaderValues(ArmoredInputStream armor) {
161        return getArmorHeaderValues(armor, HEADER_COMMENT);
162    }
163
164    public static List<String> getMessageIdHeaderValues(ArmoredInputStream armor) {
165        return getArmorHeaderValues(armor, HEADER_MESSAGEID);
166    }
167
168    public static List<String> getHashHeaderValues(ArmoredInputStream armor) {
169        return getArmorHeaderValues(armor, HEADER_HASH);
170    }
171
172    public static List<HashAlgorithm> getHashAlgorithms(ArmoredInputStream armor) {
173        List<String> algorithmNames = getHashHeaderValues(armor);
174        List<HashAlgorithm> algorithms = new ArrayList<>();
175        for (String name : algorithmNames) {
176            HashAlgorithm algorithm = HashAlgorithm.fromName(name);
177            if (algorithm != null) {
178                algorithms.add(algorithm);
179            }
180        }
181        return algorithms;
182    }
183
184    public static List<String> getVersionHeaderValues(ArmoredInputStream armor) {
185        return getArmorHeaderValues(armor, HEADER_VERSION);
186    }
187
188    public static List<String> getCharsetHeaderValues(ArmoredInputStream armor) {
189        return getArmorHeaderValues(armor, HEADER_CHARSET);
190    }
191
192    public static List<String> getArmorHeaderValues(ArmoredInputStream armor, String headerKey) {
193        String[] header = armor.getArmorHeaders();
194        String key = headerKey + ": ";
195        List<String> values = new ArrayList<>();
196        for (String line : header) {
197            if (line.startsWith(key)) {
198                values.add(line.substring(key.length()));
199            }
200        }
201        return values;
202    }
203
204    /**
205     * Hacky workaround for #96.
206     * For {@link PGPPublicKeyRingCollection#PGPPublicKeyRingCollection(InputStream, KeyFingerPrintCalculator)}
207     * or {@link PGPSecretKeyRingCollection#PGPSecretKeyRingCollection(InputStream, KeyFingerPrintCalculator)}
208     * to read all PGPKeyRings properly, we apparently have to make sure that the {@link InputStream} that is given
209     * as constructor argument is a PGPUtil.BufferedInputStreamExt.
210     * Since {@link PGPUtil#getDecoderStream(InputStream)} will return an {@link org.bouncycastle.bcpg.ArmoredInputStream}
211     * if the underlying input stream contains armored data, we have to nest two method calls to make sure that the
212     * end-result is a PGPUtil.BufferedInputStreamExt.
213     *
214     * This is a hacky solution.
215     *
216     * @param inputStream input stream
217     * @return BufferedInputStreamExt
218     */
219    public static InputStream getDecoderStream(InputStream inputStream) throws IOException {
220        BufferedInputStream buf = new BufferedInputStream(inputStream, 512);
221        InputStream decoderStream = PGPUtilWrapper.getDecoderStream(buf);
222        // Data is not armored -> return
223        if (decoderStream instanceof BufferedInputStream) {
224            return decoderStream;
225        }
226        // Wrap armored input stream with fix for #159
227        decoderStream = CRCingArmoredInputStreamWrapper.possiblyWrap(decoderStream);
228
229        decoderStream = PGPUtil.getDecoderStream(decoderStream);
230        return decoderStream;
231    }
232}