From 16b0d0730ef726915c6935c9ee84b1c7db7ccf9d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Mar 2022 14:17:35 +0100 Subject: [PATCH] Annotate and document ArmorUtils class --- .../java/org/pgpainless/util/ArmorUtils.java | 465 ++++++++++++++---- .../org/pgpainless/sop/ExtractCertImpl.java | 2 +- 2 files changed, 367 insertions(+), 100 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index a46dd211..8e93ce6d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -31,6 +31,9 @@ import org.bouncycastle.util.io.Streams; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.key.OpenPgpFingerprint; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + public final class ArmorUtils { // MessageIDs are 32 printable characters @@ -46,27 +49,79 @@ public final class ArmorUtils { } - public static String toAsciiArmoredString(PGPSecretKey secretKey) throws IOException { + /** + * Return the ASCII armored encoding of the given {@link PGPSecretKey}. + * + * @param secretKey secret key + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPSecretKey secretKey) + throws IOException { MultiMap header = keyToHeader(secretKey.getPublicKey()); return toAsciiArmoredString(secretKey.getEncoded(), header); } - public static String toAsciiArmoredString(PGPPublicKey publicKey) throws IOException { + /** + * Return the ASCII armored encoding of the given {@link PGPPublicKey}. + * + * @param publicKey public key + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPPublicKey publicKey) + throws IOException { MultiMap header = keyToHeader(publicKey); return toAsciiArmoredString(publicKey.getEncoded(), header); } - public static String toAsciiArmoredString(PGPSecretKeyRing secretKeys) throws IOException { + /** + * Return the ASCII armored encoding of the given {@link PGPSecretKeyRing}. + * + * @param secretKeys secret key ring + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPSecretKeyRing secretKeys) + throws IOException { MultiMap header = keysToHeader(secretKeys); return toAsciiArmoredString(secretKeys.getEncoded(), header); } - public static String toAsciiArmoredString(PGPPublicKeyRing publicKeys) throws IOException { + /** + * Return the ASCII armored encoding of the given {@link PGPPublicKeyRing}. + * + * @param publicKeys public key ring + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPPublicKeyRing publicKeys) + throws IOException { MultiMap header = keysToHeader(publicKeys); return toAsciiArmoredString(publicKeys.getEncoded(), header); } - public static String toAsciiArmoredString(PGPSecretKeyRingCollection secretKeyRings) throws IOException { + /** + * Return the ASCII armored encoding of the given {@link PGPSecretKeyRingCollection}. + * The encoding will use per-key ASCII armors protecting each {@link PGPSecretKeyRing} individually. + * Those armors are then concatenated with newlines in between. + * + * @param secretKeyRings secret key ring collection + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPSecretKeyRingCollection secretKeyRings) + throws IOException { StringBuilder sb = new StringBuilder(); for (Iterator iterator = secretKeyRings.iterator(); iterator.hasNext(); ) { PGPSecretKeyRing secretKeyRing = iterator.next(); @@ -78,24 +133,19 @@ public final class ArmorUtils { return sb.toString(); } - public static ArmoredOutputStream toAsciiArmoredStream(PGPKeyRing keyRing, OutputStream outputStream) { - MultiMap header = keysToHeader(keyRing); - return toAsciiArmoredStream(outputStream, header); - } - - public static ArmoredOutputStream toAsciiArmoredStream(OutputStream outputStream, MultiMap header) { - ArmoredOutputStream armoredOutputStream = ArmoredOutputStreamFactory.get(outputStream); - if (header != null) { - for (String headerKey : header.keySet()) { - for (String headerValue : header.get(headerKey)) { - armoredOutputStream.addHeader(headerKey, headerValue); - } - } - } - return armoredOutputStream; - } - - public static String toAsciiArmoredString(PGPPublicKeyRingCollection publicKeyRings) throws IOException { + /** + * Return the ASCII armored encoding of the given {@link PGPPublicKeyRingCollection}. + * The encoding will use per-key ASCII armors protecting each {@link PGPPublicKeyRing} individually. + * Those armors are then concatenated with newlines in between. + * + * @param publicKeyRings public key ring collection + * @return ascii armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPPublicKeyRingCollection publicKeyRings) + throws IOException { StringBuilder sb = new StringBuilder(); for (Iterator iterator = publicKeyRings.iterator(); iterator.hasNext(); ) { PGPPublicKeyRing publicKeyRing = iterator.next(); @@ -107,7 +157,204 @@ public final class ArmorUtils { return sb.toString(); } - private static Tuple getPrimaryUserIdAndUserIdCount(PGPPublicKey publicKey) { + /** + * Return the ASCII armored encoding of the given OpenPGP data bytes. + * + * @param bytes openpgp data + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull byte[] bytes) + throws IOException { + return toAsciiArmoredString(bytes, null); + } + + /** + * Return the ASCII armored encoding of the given OpenPGP data bytes. + * The ASCII armor will include headers from the header map. + * + * @param bytes OpenPGP data + * @param additionalHeaderValues header map + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull byte[] bytes, + @Nullable MultiMap additionalHeaderValues) + throws IOException { + return toAsciiArmoredString(new ByteArrayInputStream(bytes), additionalHeaderValues); + } + + /** + * Return the ASCII armored encoding of the {@link InputStream} containing OpenPGP data. + * + * @param inputStream input stream of OpenPGP data + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull InputStream inputStream) + throws IOException { + return toAsciiArmoredString(inputStream, null); + } + + /** + * Return the ASCII armored encoding of the OpenPGP data from the given {@link InputStream}. + * The ASCII armor will include armor headers from the given header map. + * + * @param inputStream input stream of OpenPGP data + * @param additionalHeaderValues ASCII armor header map + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull InputStream inputStream, + @Nullable MultiMap additionalHeaderValues) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armor = toAsciiArmoredStream(out, additionalHeaderValues); + Streams.pipeAll(inputStream, armor); + armor.close(); + + return out.toString(); + } + + /** + * Return an {@link ArmoredOutputStream} prepared with headers for the given key ring, which wraps the given + * {@link OutputStream}. + * + * The armored output stream can be used to encode the key ring by calling {@link PGPKeyRing#encode(OutputStream)} + * with the armored output stream as an argument. + * + * @param keyRing key ring + * @param outputStream wrapped output stream + * @return armored output stream + */ + @Nonnull + public static ArmoredOutputStream toAsciiArmoredStream(@Nonnull PGPKeyRing keyRing, + @Nonnull OutputStream outputStream) { + MultiMap header = keysToHeader(keyRing); + return toAsciiArmoredStream(outputStream, header); + } + + /** + * Create an {@link ArmoredOutputStream} wrapping the given {@link OutputStream}. + * The armored output stream will be prepared with armor headers given by header. + * + * Note: Since the armored output stream is retrieved from {@link ArmoredOutputStreamFactory#get(OutputStream)}, + * it may already come with custom headers. Hence, the header entries given by header are appended below those + * already populated headers. + * + * @param outputStream output stream to wrap + * @param header map of header entries + * @return armored output stream + */ + @Nonnull + public static ArmoredOutputStream toAsciiArmoredStream(@Nonnull OutputStream outputStream, + @Nullable MultiMap header) { + ArmoredOutputStream armoredOutputStream = ArmoredOutputStreamFactory.get(outputStream); + if (header != null) { + for (String headerKey : header.keySet()) { + for (String headerValue : header.get(headerKey)) { + armoredOutputStream.addHeader(headerKey, headerValue); + } + } + } + return armoredOutputStream; + } + + /** + * Return an {@link ArmoredOutputStream} prepared with headers for the given key ring, which wraps the given + * {@link OutputStream}. + * + * The armored output stream can be used to encode the key ring by calling {@link PGPKeyRing#encode(OutputStream)} + * with the armored output stream as an argument. + * + * @param keyRing key ring + * @param outputStream wrapped output stream + * @return armored output stream + * + * @deprecated use {@link #toAsciiArmoredStream(PGPKeyRing, OutputStream)} instead + */ + @Deprecated + @Nonnull + public static ArmoredOutputStream createArmoredOutputStreamFor(@Nonnull PGPKeyRing keyRing, + @Nonnull OutputStream outputStream) { + return toAsciiArmoredStream(keyRing, outputStream); + } + + /** + * Generate a header map for ASCII armor from the given {@link PGPKeyRing}. + * + * @param keyRing key ring + * @return header map + */ + @Nonnull + private static MultiMap keysToHeader(@Nonnull PGPKeyRing keyRing) { + PGPPublicKey publicKey = keyRing.getPublicKey(); + return keyToHeader(publicKey); + } + + /** + * Generate a header map for ASCII armor from the given {@link PGPPublicKey}. + * The header map consists of a comment field of the keys pretty-printed fingerprint, + * as well as some optional user-id information (see {@link #setUserIdInfoOnHeader(MultiMap, PGPPublicKey)}. + * + * @param publicKey public key + * @return header map + */ + @Nonnull + private static MultiMap keyToHeader(@Nonnull PGPPublicKey publicKey) { + MultiMap header = new MultiMap<>(); + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); + + header.put(HEADER_COMMENT, fingerprint.prettyPrint()); + setUserIdInfoOnHeader(header, publicKey); + return header; + } + + /** + * Add user-id information to the header map. + * If the key is carrying at least one user-id, we add a comment for the probable primary user-id. + * If the key carries more than one user-id, we further add a comment stating how many further identities + * the key has. + * + * @param header header map + * @param publicKey public key + */ + private static void setUserIdInfoOnHeader(@Nonnull MultiMap header, + @Nonnull PGPPublicKey publicKey) { + Tuple idCount = getPrimaryUserIdAndUserIdCount(publicKey); + String primary = idCount.getA(); + int totalCount = idCount.getB(); + if (primary != null) { + header.put(HEADER_COMMENT, primary); + } + if (totalCount == 2) { + header.put(HEADER_COMMENT, "1 further identity"); + } else if (totalCount > 2) { + header.put(HEADER_COMMENT, String.format("%d further identities", totalCount - 1)); + } + } + + /** + * Determine a probable primary user-id, as well as the total number of user-ids on the given {@link PGPPublicKey}. + * This method is trimmed for efficiency and does not do any cryptographic validation of signatures. + * + * The key might not have any user-id at all, in which case {@link Tuple#getA()} will return null. + * The key might have some user-ids, but none of it marked as primary, in which case {@link Tuple#getA()} + * will return the first user-id of the key. + * + * @param publicKey public key + * @return tuple consisting of a primary user-id candidate, and the total number of user-ids on the key. + */ + @Nonnull + private static Tuple getPrimaryUserIdAndUserIdCount(@Nonnull PGPPublicKey publicKey) { // Quickly determine the primary user-id + number of total user-ids // NOTE: THIS METHOD DOES NOT CRYPTOGRAPHICALLY VERIFY THE SIGNATURES // DO NOT RELY ON IT! @@ -140,98 +387,93 @@ public final class ArmorUtils { return new Tuple<>(printed, countIdentities); } - private static MultiMap keyToHeader(PGPPublicKey publicKey) { - MultiMap header = new MultiMap<>(); - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); - - header.put(HEADER_COMMENT, fingerprint.prettyPrint()); - setUserIdInfoOnHeader(header, publicKey); - return header; - } - - private static void setUserIdInfoOnHeader(MultiMap header, PGPPublicKey publicKey) { - Tuple idCount = getPrimaryUserIdAndUserIdCount(publicKey); - String primary = idCount.getA(); - int totalCount = idCount.getB(); - if (primary != null) { - header.put(HEADER_COMMENT, primary); - } - if (totalCount == 2) { - header.put(HEADER_COMMENT, "1 further identity"); - } else if (totalCount > 2) { - header.put(HEADER_COMMENT, String.format("%d further identities", totalCount - 1)); - } - } - - private static MultiMap keysToHeader(PGPKeyRing keyRing) { - PGPPublicKey publicKey = keyRing.getPublicKey(); - return keyToHeader(publicKey); - } - - public static String toAsciiArmoredString(byte[] bytes) throws IOException { - return toAsciiArmoredString(bytes, null); - } - - public static String toAsciiArmoredString(byte[] bytes, MultiMap additionalHeaderValues) throws IOException { - return toAsciiArmoredString(new ByteArrayInputStream(bytes), additionalHeaderValues); - } - - public static String toAsciiArmoredString(InputStream inputStream) throws IOException { - return toAsciiArmoredString(inputStream, null); - } - - public static void addHashAlgorithmHeader(ArmoredOutputStream armor, HashAlgorithm hashAlgorithm) { + /** + * Add an ASCII armor header entry about the used hash algorithm into the {@link ArmoredOutputStream}. + * + * @param armor armored output stream + * @param hashAlgorithm hash algorithm + * + * @see + * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + */ + public static void addHashAlgorithmHeader(@Nonnull ArmoredOutputStream armor, + @Nonnull HashAlgorithm hashAlgorithm) { armor.addHeader(HEADER_HASH, hashAlgorithm.getAlgorithmName()); } - public static void addCommentHeader(ArmoredOutputStream armor, String comment) { + /** + * Add an ASCII armor comment header entry into the {@link ArmoredOutputStream}. + * + * @param armor armored output stream + * @param comment free-text comment + * + * @see + * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + */ + public static void addCommentHeader(@Nonnull ArmoredOutputStream armor, + @Nonnull String comment) { armor.addHeader(HEADER_COMMENT, comment); } - public static void addMessageIdHeader(ArmoredOutputStream armor, String messageId) { - if (messageId == null) { - throw new NullPointerException("MessageID cannot be null."); - } + /** + * Add an ASCII armor message-id header entry into the {@link ArmoredOutputStream}. + * + * @param armor armored output stream + * @param messageId message id + * + * @see + * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + */ + public static void addMessageIdHeader(@Nonnull ArmoredOutputStream armor, + @Nonnull String messageId) { if (!PATTERN_MESSAGE_ID.matcher(messageId).matches()) { throw new IllegalArgumentException("MessageIDs MUST consist of 32 printable characters."); } armor.addHeader(HEADER_MESSAGEID, messageId); } - public static String toAsciiArmoredString(InputStream inputStream, MultiMap additionalHeaderValues) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armor = toAsciiArmoredStream(out, additionalHeaderValues); - Streams.pipeAll(inputStream, armor); - armor.close(); - - return out.toString(); - } - - public static ArmoredOutputStream createArmoredOutputStreamFor(PGPKeyRing keyRing, OutputStream outputStream) { - ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(outputStream); - MultiMap headerMap = keysToHeader(keyRing); - for (String header : headerMap.keySet()) { - for (String value : headerMap.get(header)) { - armor.addHeader(header, value); - } - } - - return armor; - } - - public static List getCommentHeaderValues(ArmoredInputStream armor) { + /** + * Extract all ASCII armor header values of type comment from the given {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @return list of comment headers + */ + @Nonnull + public static List getCommentHeaderValues(@Nonnull ArmoredInputStream armor) { return getArmorHeaderValues(armor, HEADER_COMMENT); } - public static List getMessageIdHeaderValues(ArmoredInputStream armor) { + /** + * Extract all ASCII armor header values of type message id from the given {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @return list of message-id headers + */ + @Nonnull + public static List getMessageIdHeaderValues(@Nonnull ArmoredInputStream armor) { return getArmorHeaderValues(armor, HEADER_MESSAGEID); } - public static List getHashHeaderValues(ArmoredInputStream armor) { + /** + * Return all ASCII armor header values of type hash-algorithm from the given {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @return list of hash headers + */ + @Nonnull + public static List getHashHeaderValues(@Nonnull ArmoredInputStream armor) { return getArmorHeaderValues(armor, HEADER_HASH); } - public static List getHashAlgorithms(ArmoredInputStream armor) { + /** + * Return a list of {@link HashAlgorithm} enums extracted from the hash header entries of the given + * {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @return list of hash algorithms from the ASCII header + */ + @Nonnull + public static List getHashAlgorithms(@Nonnull ArmoredInputStream armor) { List algorithmNames = getHashHeaderValues(armor); List algorithms = new ArrayList<>(); for (String name : algorithmNames) { @@ -243,15 +485,38 @@ public final class ArmorUtils { return algorithms; } - public static List getVersionHeaderValues(ArmoredInputStream armor) { + /** + * Return all ASCII armor header values of type version from the given {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @return list of version headers + */ + @Nonnull + public static List getVersionHeaderValues(@Nonnull ArmoredInputStream armor) { return getArmorHeaderValues(armor, HEADER_VERSION); } - public static List getCharsetHeaderValues(ArmoredInputStream armor) { + /** + * Return all ASCII armor header values of type charset from the given {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @return list of charset headers + */ + @Nonnull + public static List getCharsetHeaderValues(@Nonnull ArmoredInputStream armor) { return getArmorHeaderValues(armor, HEADER_CHARSET); } - public static List getArmorHeaderValues(ArmoredInputStream armor, String headerKey) { + /** + * Return all ASCII armor header values of the given headerKey from the given {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @param headerKey ASCII armor header key + * @return list of values for the header key + */ + @Nonnull + public static List getArmorHeaderValues(@Nonnull ArmoredInputStream armor, + @Nonnull String headerKey) { String[] header = armor.getArmorHeaders(); String key = headerKey + ": "; List values = new ArrayList<>(); @@ -278,7 +543,9 @@ public final class ArmorUtils { * @param inputStream input stream * @return BufferedInputStreamExt */ - public static InputStream getDecoderStream(InputStream inputStream) throws IOException { + @Nonnull + public static InputStream getDecoderStream(@Nonnull InputStream inputStream) + throws IOException { BufferedInputStream buf = new BufferedInputStream(inputStream, 512); InputStream decoderStream = PGPUtilWrapper.getDecoderStream(buf); // Data is not armored -> return diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java index 6c3c825f..5f694208 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java @@ -54,7 +54,7 @@ public class ExtractCertImpl implements ExtractCert { public void writeTo(OutputStream outputStream) throws IOException { for (PGPPublicKeyRing cert : certs) { - OutputStream out = armor ? ArmorUtils.createArmoredOutputStreamFor(cert, outputStream) : outputStream; + OutputStream out = armor ? ArmorUtils.toAsciiArmoredStream(cert, outputStream) : outputStream; cert.encode(out); if (armor) {