mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-11-14 16:32:06 +01:00
Kotlin conversion: ArmorUtils
This commit is contained in:
parent
b9e6453f62
commit
50cc71b75d
3 changed files with 430 additions and 603 deletions
|
@ -1,603 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<PGPSecretKeyRing> 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<PGPPublicKeyRing> 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 <pre>true</pre> 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 <pre>export</pre> is true, the signature will be stripped of non-exportable subpackets or trust-packets.
|
||||
* If it is <pre>false</pre>, 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> keyToHeader(@Nonnull PGPPublicKey publicKey) {
|
||||
MultiMap<String, String> 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<String, String> header,
|
||||
@Nonnull PGPPublicKey publicKey) {
|
||||
Tuple<String, Integer> 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<String, Integer> 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<String> 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<PGPSignature> 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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-6.2">
|
||||
* RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor</a>
|
||||
*/
|
||||
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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-6.2">
|
||||
* RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor</a>
|
||||
*/
|
||||
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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-6.2">
|
||||
* RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor</a>
|
||||
*/
|
||||
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<String> 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<String> 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<String> 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<HashAlgorithm> getHashAlgorithms(@Nonnull ArmoredInputStream armor) {
|
||||
List<String> algorithmNames = getHashHeaderValues(armor);
|
||||
List<HashAlgorithm> 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<String> 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<String> 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<String> getArmorHeaderValues(@Nonnull ArmoredInputStream armor,
|
||||
@Nonnull String headerKey) {
|
||||
String[] header = armor.getArmorHeaders();
|
||||
String key = headerKey + ": ";
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,14 @@
|
|||
|
||||
package org.pgpainless.util;
|
||||
|
||||
/**
|
||||
* Helper class pairing together two values.
|
||||
* @param <A> type of the first value
|
||||
* @param <B> type of the second value
|
||||
* @deprecated Scheduled for removal.
|
||||
* TODO: Remove
|
||||
*/
|
||||
@Deprecated
|
||||
public class Tuple<A, B> {
|
||||
|
||||
private final A a;
|
||||
|
|
|
@ -0,0 +1,422 @@
|
|||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<String, Set<String>>? = 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<String, Set<String>>? = 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<String, Set<String>>? = 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<String, Set<String>> {
|
||||
val headerMap = mutableMapOf<String, MutableSet<String>>()
|
||||
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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-6.2">
|
||||
* RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor</a>
|
||||
*/
|
||||
@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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-6.2">
|
||||
* RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor</a>
|
||||
*/
|
||||
@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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-6.2">
|
||||
* RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor</a>
|
||||
*/
|
||||
@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<String> =
|
||||
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<String> =
|
||||
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<String> =
|
||||
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<HashAlgorithm> =
|
||||
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<String> =
|
||||
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<String> =
|
||||
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<String> =
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue