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}