From b324742a623cbaa819b91af4de846ffb56dff1c1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 28 Sep 2023 13:30:39 +0200 Subject: [PATCH] Kotlin conversion: ArmorUtils --- .../java/org/pgpainless/util/ArmorUtils.java | 603 ------------------ .../main/java/org/pgpainless/util/Tuple.java | 8 + .../kotlin/org/pgpainless/util/ArmorUtils.kt | 422 ++++++++++++ 3 files changed, 430 insertions(+), 603 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java deleted file mode 100644 index 976f6ab2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ /dev/null @@ -1,603 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.regex.Pattern; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.decryption_verification.OpenPgpInputStream; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.util.KeyRingUtils; - -/** - * Utility class for dealing with ASCII armored OpenPGP data. - */ -public final class ArmorUtils { - - // MessageIDs are 32 printable characters - private static final Pattern PATTERN_MESSAGE_ID = Pattern.compile("^\\S{32}$"); - - /** - * Constant armor key for comments. - */ - public static final String HEADER_COMMENT = "Comment"; - /** - * Constant armor key for program versions. - */ - public static final String HEADER_VERSION = "Version"; - /** - * Constant armor key for message IDs. Useful for split messages. - */ - public static final String HEADER_MESSAGEID = "MessageID"; - /** - * Constant armor key for used hash algorithms in clearsigned messages. - */ - public static final String HEADER_HASH = "Hash"; - /** - * Constant armor key for message character sets. - */ - public static final String HEADER_CHARSET = "Charset"; - - private ArmorUtils() { - - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * 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(); - sb.append(toAsciiArmoredString(secretKeyRing)); - if (iterator.hasNext()) { - sb.append('\n'); - } - } - return sb.toString(); - } - - /** - * 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(); - sb.append(toAsciiArmoredString(publicKeyRing)); - if (iterator.hasNext()) { - sb.append('\n'); - } - } - return sb.toString(); - } - - /** - * Return the ASCII armored representation of the given detached signature. - * The signature will not be stripped of non-exportable subpackets or trust-packets. - * If you need to strip those (e.g. because the signature is intended to be sent to a third party), use - * {@link #toAsciiArmoredString(PGPSignature, boolean)} and provide
true
as boolean value. - * - * @param signature signature - * @return ascii armored string - * - * @throws IOException in case of an error in the {@link ArmoredOutputStream} - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPSignature signature) throws IOException { - return toAsciiArmoredString(signature, false); - } - - /** - * Return the ASCII armored representation of the given detached signature. - * If
export
is true, the signature will be stripped of non-exportable subpackets or trust-packets. - * If it is
false
, the signature will be encoded as-is. - * - * @param signature signature - * @param export whether to exclude non-exportable subpackets or trust-packets. - * @return ascii armored string - * - * @throws IOException in case of an error in the {@link ArmoredOutputStream} - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPSignature signature, boolean export) - throws IOException { - return toAsciiArmoredString(signature.getEncoded(export)); - } - - /** - * 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; - } - - /** - * 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! - List userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey); - int countIdentities = 0; - String first = null; - String primary = null; - for (String userId : userIds) { - countIdentities++; - // remember the first user-id - if (first == null) { - first = userId; - } - - if (primary == null) { - Iterator signatures = publicKey.getSignaturesForID(userId); - while (signatures.hasNext()) { - PGPSignature signature = signatures.next(); - if (signature.getHashedSubPackets().isPrimaryUserID()) { - primary = userId; - break; - } - } - } - } - // It may happen that no user-id is marked as primary - // in that case print the first one - String printed = primary != null ? primary : first; - return new Tuple<>(printed, countIdentities); - } - - /** - * Set the version header entry in the ASCII armor. - * If the version info is null or only contains whitespace characters, then the version header will be removed. - * - * @param armor armored output stream - * @param version version header. - */ - public static void setVersionHeader(@Nonnull ArmoredOutputStream armor, - @Nullable String version) { - if (version == null || version.trim().isEmpty()) { - armor.setHeader(HEADER_VERSION, null); - } else { - armor.setHeader(HEADER_VERSION, version); - } - } - - /** - * 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()); - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * 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) { - HashAlgorithm algorithm = HashAlgorithm.fromName(name); - if (algorithm != null) { - algorithms.add(algorithm); - } - } - return algorithms; - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * 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<>(); - for (String line : header) { - if (line.startsWith(key)) { - values.add(line.substring(key.length())); - } - } - return values; - } - - /** - * Hacky workaround for #96. - * For {@link PGPPublicKeyRingCollection#PGPPublicKeyRingCollection(InputStream, KeyFingerPrintCalculator)} - * or {@link PGPSecretKeyRingCollection#PGPSecretKeyRingCollection(InputStream, KeyFingerPrintCalculator)} - * to read all PGPKeyRings properly, we apparently have to make sure that the {@link InputStream} that is given - * as constructor argument is a PGPUtil.BufferedInputStreamExt. - * Since {@link PGPUtil#getDecoderStream(InputStream)} will return an {@link org.bouncycastle.bcpg.ArmoredInputStream} - * if the underlying input stream contains armored data, we first dearmor the data ourselves to make sure that the - * end-result is a PGPUtil.BufferedInputStreamExt. - * - * @param inputStream input stream - * @return BufferedInputStreamExt - * - * @throws IOException in case of an IO error - */ - @Nonnull - public static InputStream getDecoderStream(@Nonnull InputStream inputStream) - throws IOException { - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); - if (openPgpIn.isAsciiArmored()) { - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); - return PGPUtil.getDecoderStream(armorIn); - } - - return openPgpIn; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java b/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java index 27ad6a12..84d7a370 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java @@ -4,6 +4,14 @@ package org.pgpainless.util; +/** + * Helper class pairing together two values. + * @param type of the first value + * @param type of the second value + * @deprecated Scheduled for removal. + * TODO: Remove + */ +@Deprecated public class Tuple { private final A a; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt new file mode 100644 index 00000000..c8ff4da8 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt @@ -0,0 +1,422 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0package org.pgpainless.util + +package org.pgpainless.util + +import org.bouncycastle.bcpg.ArmoredInputStream +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.util.io.Streams +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.util.KeyRingUtils +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +class ArmorUtils { + + companion object { + // MessageIDs are 32 printable characters + private val PATTER_MESSAGE_ID = "^\\S{32}$".toRegex() + /** + * Constant armor key for comments. + */ + const val HEADER_COMMENT = "Comment" + /** + * Constant armor key for program versions. + */ + const val HEADER_VERSION = "Version" + /** + * Constant armor key for message IDs. Useful for split messages. + */ + const val HEADER_MESSAGEID = "MessageID" + /** + * Constant armor key for used hash algorithms in clearsigned messages. + */ + const val HEADER_HASH = "Hash" + /** + * Constant armor key for message character sets. + */ + const val HEADER_CHARSET = "Charset" + + /** + * Return the ASCII armored encoding of the given [PGPSecretKey]. + * + * @param secretKey secret key + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(secretKey: PGPSecretKey): String = + toAsciiArmoredString(secretKey.encoded, keyToHeader(secretKey.publicKey)) + + /** + * Return the ASCII armored encoding of the given [PGPPublicKey]. + * + * @param publicKey public key + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(publicKey: PGPPublicKey): String = + toAsciiArmoredString(publicKey.encoded, keyToHeader(publicKey)) + + /** + * Return the ASCII armored encoding of the given [PGPSecretKeyRing]. + * + * @param secretKeys secret key ring + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(secretKeys: PGPSecretKeyRing): String = + toAsciiArmoredString(secretKeys.encoded, keyToHeader(secretKeys.publicKey)) + + /** + * Return the ASCII armored encoding of the given [PGPPublicKeyRing]. + * + * @param certificate public key ring + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(certificate: PGPPublicKeyRing): String = + toAsciiArmoredString(certificate.encoded, keyToHeader(certificate.publicKey)) + + /** + * Return the ASCII armored encoding of the given [PGPSecretKeyRingCollection]. + * The encoding will use per-key ASCII armors protecting each [PGPSecretKeyRing] individually. + * Those armors are then concatenated with newlines in between. + * + * @param secretKeysCollection secret key ring collection + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(secretKeysCollection: PGPSecretKeyRingCollection): String = + secretKeysCollection.keyRings.asSequence() + .joinToString("\n") { toAsciiArmoredString(it) } + + /** + * Return the ASCII armored encoding of the given [PGPPublicKeyRingCollection]. + * The encoding will use per-key ASCII armors protecting each [PGPPublicKeyRing] individually. + * Those armors are then concatenated with newlines in between. + * + * @param certificates public key ring collection + * @return ascii armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(certificates: PGPPublicKeyRingCollection): String = + certificates.joinToString("\n") { toAsciiArmoredString(it) } + + /** + * Return the ASCII armored representation of the given detached signature. + * If [export] is true, the signature will be stripped of non-exportable subpackets or trust-packets. + * If it is false, the signature will be encoded as-is. + * + * @param signature signature + * @param export whether to exclude non-exportable subpackets or trust-packets. + * @return ascii armored string + * + * @throws IOException in case of an error in the [ArmoredOutputStream] + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun toAsciiArmoredString(signature: PGPSignature, export: Boolean = false): String = + toAsciiArmoredString(signature.getEncoded(export)) + + /** + * 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 header header map + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun toAsciiArmoredString(bytes: ByteArray, header: Map>? = null): String = + toAsciiArmoredString(bytes.inputStream(), header) + + /** + * 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 header ASCII armor header map + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun toAsciiArmoredString(inputStream: InputStream, header: Map>? = null): String = + ByteArrayOutputStream().apply { + toAsciiArmoredStream(this, header).run { + Streams.pipeAll(inputStream, this) + this.close() + } + }.toString() + + /** + * Return an [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 [PGPKeyRing.encode] + * with the armored output stream as an argument. + * + * @param keys OpenPGP key or certificate + * @param outputStream wrapped output stream + * @return armored output stream + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredStream(keys: PGPKeyRing, outputStream: OutputStream): ArmoredOutputStream = + toAsciiArmoredStream(outputStream, keyToHeader(keys.publicKey)) + + /** + * Create an [ArmoredOutputStream] wrapping the given [OutputStream]. + * The armored output stream will be prepared with armor headers given by header. + * + * Note: Since the armored output stream is retrieved from [ArmoredOutputStreamFactory.get], + * 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 + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun toAsciiArmoredStream(outputStream: OutputStream, header: Map>? = null): ArmoredOutputStream = + ArmoredOutputStreamFactory.get(outputStream).apply { + header?.forEach { entry -> + entry.value.forEach { value -> + addHeader(entry.key, value) + } + } + } + + /** + * Generate a header map for ASCII armor from the given [PGPPublicKey]. + * The header map consists of a comment field of the keys pretty-printed fingerprint, + * as well as the primary or first user-id plus the count of remaining user-ids. + * + * @param publicKey public key + * @return header map + */ + @JvmStatic + private fun keyToHeader(publicKey: PGPPublicKey): Map> { + val headerMap = mutableMapOf>() + val userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey) + val first: String? = userIds.firstOrNull() + val primary: String? = userIds.firstOrNull { + publicKey.getSignaturesForID(it)?.asSequence()?.any { sig -> + sig.hashedSubPackets.isPrimaryUserID + } ?: false + } + + // Fingerprint + headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add(OpenPgpFingerprint.of(publicKey).prettyPrint()) + // Primary / First User ID + (primary ?: first)?.let { headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add(it) } + // X-1 further identities + when (userIds.size) { + 0, 1 -> {} + 2 -> headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add("1 further identity") + else -> headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add("${userIds.size - 1} further identities") + } + return headerMap + } + + /** + * Set the version header entry in the ASCII armor. + * If the version info is null or only contains whitespace characters, then the version header will be removed. + * + * @param armor armored output stream + * @param version version header. + */ + @JvmStatic + @Deprecated("Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + "Use ArmoredOutputStream builder instead.") + fun setVersionHeader(armor: ArmoredOutputStream, version: String?) = + armor.setHeader(HEADER_VERSION, version?.let { it.ifBlank { null } }) + + /** + * Add an ASCII armor header entry about the used hash algorithm into the [ArmoredOutputStream]. + * + * @param armor armored output stream + * @param hashAlgorithm hash algorithm + * + * @see + * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + */ + @JvmStatic + @Deprecated("Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + "Use ArmoredOutputStream builder instead.") + fun addHashAlgorithmHeader(armor: ArmoredOutputStream, hashAlgorithm: HashAlgorithm) = + armor.addHeader(HEADER_HASH, hashAlgorithm.algorithmName) + + /** + * Add an ASCII armor comment header entry into the [ArmoredOutputStream]. + * + * @param armor armored output stream + * @param comment free-text comment + * + * @see + * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + */ + @JvmStatic + @Deprecated("Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + "Use ArmoredOutputStream builder instead.") + fun addCommentHeader(armor: ArmoredOutputStream, comment: String) = + armor.addHeader(HEADER_COMMENT, comment) + + /** + * Add an ASCII armor message-id header entry into the [ArmoredOutputStream]. + * + * @param armor armored output stream + * @param messageId message id + * + * @see + * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + */ + @JvmStatic + @Deprecated("Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + "Use ArmoredOutputStream builder instead.") + fun addMessageIdHeader(armor: ArmoredOutputStream, messageId: String) { + require(PATTER_MESSAGE_ID.matches(messageId)) { "MessageIDs MUST consist of 32 printable characters." } + armor.addHeader(HEADER_MESSAGEID, messageId) + } + + /** + * Extract all ASCII armor header values of type comment from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of comment headers + */ + @JvmStatic + fun getCommentHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_COMMENT) + + /** + * Extract all ASCII armor header values of type message id from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of message-id headers + */ + @JvmStatic + fun getMessageIdHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_MESSAGEID) + + /** + * Return all ASCII armor header values of type hash-algorithm from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of hash headers + */ + @JvmStatic + fun getHashHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_HASH) + + /** + * Return a list of [HashAlgorithm] enums extracted from the hash header entries of the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of hash algorithms from the ASCII header + */ + @JvmStatic + fun getHashAlgorithms(armor: ArmoredInputStream): List = + getHashHeaderValues(armor).mapNotNull { HashAlgorithm.fromName(it) } + + /** + * Return all ASCII armor header values of type version from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of version headers + */ + @JvmStatic + fun getVersionHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_VERSION) + + /** + * Return all ASCII armor header values of type charset from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of charset headers + */ + @JvmStatic + fun getCharsetHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_CHARSET) + + /** + * Return all ASCII armor header values of the given headerKey from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @param key ASCII armor header key + * @return list of values for the header key + */ + @JvmStatic + fun getArmorHeaderValues(armor: ArmoredInputStream, key: String): List = + armor.armorHeaders + .filter { it.startsWith("$key: ") } + .map { it.substring(key.length + 2) } // key.len + ": ".len + + /** + * Hacky workaround for #96. + * For `PGPPublicKeyRingCollection(InputStream, KeyFingerPrintCalculator)` + * or `PGPSecretKeyRingCollection(InputStream, KeyFingerPrintCalculator)` + * to read all PGPKeyRings properly, we apparently have to make sure that the [InputStream] that is given + * as constructor argument is a [PGPUtil.BufferedInputStreamExt]. + * Since [PGPUtil.getDecoderStream] will return an [org.bouncycastle.bcpg.ArmoredInputStream] + * if the underlying input stream contains armored data, we first dearmor the data ourselves to make sure that the + * end-result is a [PGPUtil.BufferedInputStreamExt]. + * + * @param inputStream input stream + * @return BufferedInputStreamExt + * + * @throws IOException in case of an IO error + */ + @JvmStatic + @Throws(IOException::class) + fun getDecoderStream(inputStream: InputStream): InputStream = + OpenPgpInputStream(inputStream).let { + if (it.isAsciiArmored) { + PGPUtil.getDecoderStream(ArmoredInputStreamFactory.get(it)) + } else { + it + } + } + } +} \ No newline at end of file