From 92d66f7f3017f6d5c3299c6d2d0c4976118cf2b9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 14 May 2025 13:27:06 +0200 Subject: [PATCH] Add OpenPGPCertificateUtil and unify the way, SOP encodes/armors certificates/keys --- .../OpenPGPCertificateExtensions.kt | 38 ++++++ .../pgpainless/util/OpenPGPCertificateUtil.kt | 47 +++++++- .../util/OpenPGPCertificateUtilTest.java | 114 ++++++++++++++++++ .../org/pgpainless/sop/CertifyUserIdImpl.kt | 69 +++++------ .../pgpainless/sop/ChangeKeyPasswordImpl.kt | 11 +- .../org/pgpainless/sop/ExtractCertImpl.kt | 16 +-- .../org/pgpainless/sop/GenerateKeyImpl.kt | 7 +- .../org/pgpainless/sop/MergeCertsImpl.kt | 19 +-- .../org/pgpainless/sop/RevokeKeyImpl.kt | 12 +- .../org/pgpainless/sop/UpdateKeyImpl.kt | 32 ++--- 10 files changed, 260 insertions(+), 105 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPCertificateExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPCertificateExtensions.kt index 886f6830..04053547 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPCertificateExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPCertificateExtensions.kt @@ -4,6 +4,9 @@ package org.pgpainless.bouncycastle.extensions +import java.io.OutputStream +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.bouncycastle.bcpg.PacketFormat import org.bouncycastle.openpgp.PGPOnePassSignature import org.bouncycastle.openpgp.api.OpenPGPCertificate import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey @@ -22,3 +25,38 @@ fun OpenPGPCertificate.getKeyVersion(): OpenPGPKeyVersion = primaryKey.getKeyVer /** Return the [OpenPGPKeyVersion] of the component key. */ fun OpenPGPComponentKey.getKeyVersion(): OpenPGPKeyVersion = OpenPGPKeyVersion.from(this.version) + +/** + * ASCII-armor-encode the certificate into the given [OutputStream]. + * + * @param outputStream output stream + * @param format packet length encoding format, defaults to [PacketFormat.ROUNDTRIP] + */ +fun OpenPGPCertificate.asciiArmor( + outputStream: OutputStream, + format: PacketFormat = PacketFormat.ROUNDTRIP +) { + outputStream.write(toAsciiArmoredString(format).encodeToByteArray()) +} + +/** + * ASCII-armor-encode the certificate into the given [OutputStream]. + * + * @param outputStream output stream + * @param format packet length encoding format, defaults to [PacketFormat.ROUNDTRIP] + * @param armorBuilder builder for the ASCII armored output stream + */ +fun OpenPGPCertificate.asciiArmor( + outputStream: OutputStream, + format: PacketFormat, + armorBuilder: ArmoredOutputStream.Builder +) { + outputStream.write(toAsciiArmoredString(format, armorBuilder).encodeToByteArray()) +} + +fun OpenPGPCertificate.encode( + outputStream: OutputStream, + format: PacketFormat = PacketFormat.ROUNDTRIP +) { + outputStream.write(getEncoded(format)) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/OpenPGPCertificateUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/OpenPGPCertificateUtil.kt index ee8ca8ac..48475547 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/OpenPGPCertificateUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/OpenPGPCertificateUtil.kt @@ -1,4 +1,49 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.util -class OpenPGPCertificateUtil { +import java.io.OutputStream +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.bouncycastle.bcpg.PacketFormat +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.pgpainless.bouncycastle.extensions.asciiArmor +import org.pgpainless.bouncycastle.extensions.encode + +class OpenPGPCertificateUtil private constructor() { + + companion object { + @JvmStatic + @JvmOverloads + fun encode( + certs: Collection, + outputStream: OutputStream, + packetFormat: PacketFormat = PacketFormat.ROUNDTRIP + ) { + for (cert in certs) { + cert.encode(outputStream, packetFormat) + } + } + + @JvmStatic + @JvmOverloads + fun armor( + certs: Collection, + outputStream: OutputStream, + packetFormat: PacketFormat = PacketFormat.ROUNDTRIP + ) { + if (certs.size == 1) { + // Add pretty armor header to single cert/key + certs.iterator().next().asciiArmor(outputStream, packetFormat) + } else { + // Do not add a pretty header + val aOut = ArmoredOutputStream(outputStream) + for (cert in certs) { + cert.encode(aOut, packetFormat) + } + aOut.close() + } + } + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/OpenPGPCertificateUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/OpenPGPCertificateUtilTest.java index 7943c195..520dc303 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/OpenPGPCertificateUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/OpenPGPCertificateUtilTest.java @@ -1,4 +1,118 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.util; +import org.bouncycastle.bcpg.PacketFormat; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.pgpainless.PGPainless; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class OpenPGPCertificateUtilTest { + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncodeSingleCert() { + PGPainless api = PGPainless.getInstance(); + + List certs = new ArrayList<>(); + certs.add(api.generateKey().modernKeyRing("Alice ").toCertificate()); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPCertificateUtil.armor(certs, bOut, PacketFormat.CURRENT); + String armor = bOut.toString(); + + assertTrue(armor.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: "), + "For a single cert, the ASCII armor MUST contain a comment with the fingerprint"); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncodeSingleKey() { + PGPainless api = PGPainless.getInstance(); + + List certs = new ArrayList<>(); + certs.add(api.generateKey().modernKeyRing("Alice ")); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPCertificateUtil.armor(certs, bOut, PacketFormat.CURRENT); + String armor = bOut.toString(); + + assertTrue(armor.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\nComment: "), + "For a single key, the ASCII armor MUST contain a comment with the fingerprint"); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncodeTwoCerts() { + PGPainless api = PGPainless.getInstance(); + + List certs = new ArrayList<>(); + certs.add(api.generateKey().modernKeyRing("Alice ").toCertificate()); + certs.add(api.generateKey().modernKeyRing("Bob ").toCertificate()); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPCertificateUtil.armor(certs, bOut, PacketFormat.CURRENT); + String armor = bOut.toString(); + + assertTrue(armor.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")); + assertEquals( + armor.indexOf("-----BEGIN PGP PUBLIC KEY BLOCK-----"), + armor.lastIndexOf("-----BEGIN PGP PUBLIC KEY BLOCK-----"), + "There MUST only be a single block in the armor."); + assertFalse(armor.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: "), + "For multiple certs, the ASCII armor MUST NOT contain a comment containing the fingerprint"); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncodeCertAndKey() { + PGPainless api = PGPainless.getInstance(); + + List certs = new ArrayList<>(); + certs.add(api.generateKey().modernKeyRing("Alice ").toCertificate()); + certs.add(api.generateKey().modernKeyRing("Bob ")); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPCertificateUtil.armor(certs, bOut, PacketFormat.CURRENT); + String armor = bOut.toString(); + + assertTrue(armor.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")); + assertEquals( + armor.indexOf("-----BEGIN PGP PUBLIC KEY BLOCK-----"), + armor.lastIndexOf("-----BEGIN PGP PUBLIC KEY BLOCK-----")); + assertFalse(armor.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: "), + "For multiple certs/keys, the ASCII armor MUST NOT contain a comment containing the fingerprint"); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncodeKeyAndCert() { + PGPainless api = PGPainless.getInstance(); + + List certs = new ArrayList<>(); + certs.add(api.generateKey().modernKeyRing("Alice ")); + certs.add(api.generateKey().modernKeyRing("Bob ").toCertificate()); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPCertificateUtil.armor(certs, bOut, PacketFormat.CURRENT); + String armor = bOut.toString(); + + assertTrue(armor.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----")); + assertEquals( + armor.indexOf("-----BEGIN PGP PRIVATE KEY BLOCK-----"), + armor.lastIndexOf("-----BEGIN PGP PRIVATE KEY BLOCK-----")); + assertFalse(armor.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\nComment: "), + "For multiple certs, the ASCII armor MUST NOT contain a comment containing the fingerprint"); + } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/CertifyUserIdImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/CertifyUserIdImpl.kt index 4ab85803..cbd97934 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/CertifyUserIdImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/CertifyUserIdImpl.kt @@ -6,11 +6,10 @@ package org.pgpainless.sop import java.io.InputStream import java.io.OutputStream -import org.bouncycastle.bcpg.PacketFormat import org.bouncycastle.openpgp.api.OpenPGPKey import org.pgpainless.PGPainless import org.pgpainless.exception.KeyException -import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.OpenPGPCertificateUtil import org.pgpainless.util.Passphrase import sop.Ready import sop.exception.SOPGPException @@ -27,45 +26,43 @@ class CertifyUserIdImpl(private val api: PGPainless) : CertifyUserId { override fun certs(certs: InputStream): Ready { return object : Ready() { override fun writeTo(outputStream: OutputStream) { - val out = - if (armor) { - ArmoredOutputStreamFactory.get(outputStream) - } else outputStream - api.readKey() - .parseCertificates(certs) - .onEach { cert -> - if (requireSelfSig) { - // Check for non-bound user-ids - userIds - .find { cert.getUserId(it)?.isBound != true } - ?.let { throw SOPGPException.CertUserIdNoMatch(cert.fingerprint) } - } - } - .forEach { cert -> - var certificate = cert - keys.forEach { key -> - userIds.forEach { userId -> - try { - certificate = - api.generateCertification() - .certifyUserId(userId, certificate) - .withKey(key, protector) - .build() - .certifiedCertificate - } catch (e: KeyException) { - throw SOPGPException.KeyCannotCertify(e) - } + val certificates = + api.readKey() + .parseCertificates(certs) + .onEach { cert -> + if (requireSelfSig) { + // Check for non-bound user-ids + userIds + .find { cert.getUserId(it)?.isBound != true } + ?.let { + throw SOPGPException.CertUserIdNoMatch(cert.fingerprint) + } } } + .map { cert -> + var certificate = cert + keys.forEach { key -> + userIds.forEach { userId -> + try { + certificate = + api.generateCertification() + .certifyUserId(userId, certificate) + .withKey(key, protector) + .build() + .certifiedCertificate + } catch (e: KeyException) { + throw SOPGPException.KeyCannotCertify(e) + } + } + } + certificate + } - out.write(certificate.getEncoded(PacketFormat.CURRENT)) - } - - out.close() if (armor) { - // armored output stream does not close inner stream - outputStream.close() + OpenPGPCertificateUtil.armor(certificates, outputStream) + } else { + OpenPGPCertificateUtil.encode(certificates, outputStream) } } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt index f8b66896..a7d0c530 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt @@ -8,12 +8,11 @@ import java.io.IOException import java.io.InputStream import java.io.OutputStream import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection import org.pgpainless.PGPainless import org.pgpainless.exception.MissingPassphraseException import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.util.KeyRingUtils -import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.OpenPGPCertificateUtil import org.pgpainless.util.Passphrase import sop.Ready import sop.exception.SOPGPException @@ -54,16 +53,14 @@ class ChangeKeyPasswordImpl(private val api: PGPainless) : ChangeKeyPassword { "Cannot change passphrase of key ${it.keyIdentifier}", e) } } - .let { PGPSecretKeyRingCollection(it) } + .map { api.toKey(it) } return object : Ready() { override fun writeTo(outputStream: OutputStream) { if (armor) { - ArmoredOutputStreamFactory.get(outputStream).use { - updatedSecretKeys.encode(it) - } + OpenPGPCertificateUtil.armor(updatedSecretKeys, outputStream) } else { - updatedSecretKeys.encode(outputStream) + OpenPGPCertificateUtil.encode(updatedSecretKeys, outputStream) } } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt index b4677a56..fc72d994 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt @@ -7,7 +7,7 @@ package org.pgpainless.sop import java.io.InputStream import java.io.OutputStream import org.pgpainless.PGPainless -import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.OpenPGPCertificateUtil import sop.Ready import sop.operation.ExtractCert @@ -22,19 +22,9 @@ class ExtractCertImpl(private val api: PGPainless) : ExtractCert { return object : Ready() { override fun writeTo(outputStream: OutputStream) { if (armor) { - if (certs.size == 1) { - val cert = certs[0] - // This way we have a nice armor header with fingerprint and user-ids - val armored = cert.toAsciiArmoredString() - outputStream.write(armored.toByteArray()) - } else { - // for multiple certs, add no info headers to the ASCII armor - val armorOut = ArmoredOutputStreamFactory.get(outputStream) - certs.forEach { armorOut.write(it.encoded) } - armorOut.close() - } + OpenPGPCertificateUtil.armor(certs, outputStream) } else { - certs.forEach { outputStream.write(it.encoded) } + OpenPGPCertificateUtil.encode(certs, outputStream) } } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt index 841a12ae..a85468ee 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt @@ -13,6 +13,8 @@ import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.api.OpenPGPKey import org.pgpainless.PGPainless import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.bouncycastle.extensions.asciiArmor +import org.pgpainless.bouncycastle.extensions.encode import org.pgpainless.key.generation.KeyRingBuilder import org.pgpainless.key.generation.KeySpec import org.pgpainless.key.generation.type.KeyType @@ -50,10 +52,9 @@ class GenerateKeyImpl(private val api: PGPainless) : GenerateKey { return object : Ready() { override fun writeTo(outputStream: OutputStream) { if (armor) { - val armored = key.toAsciiArmoredString(PacketFormat.CURRENT) - outputStream.write(armored.toByteArray()) + key.asciiArmor(outputStream, PacketFormat.CURRENT) } else { - outputStream.write(key.getEncoded(PacketFormat.CURRENT)) + key.encode(outputStream, PacketFormat.CURRENT) } } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MergeCertsImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MergeCertsImpl.kt index 13fd0065..dc429c9e 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MergeCertsImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MergeCertsImpl.kt @@ -9,7 +9,7 @@ import java.io.OutputStream import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.api.OpenPGPCertificate import org.pgpainless.PGPainless -import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.OpenPGPCertificateUtil import sop.Ready import sop.operation.MergeCerts @@ -46,22 +46,11 @@ class MergeCertsImpl(private val api: PGPainless) : MergeCerts { baseCerts[update.keyIdentifier] = api.mergeCertificate(baseCert, update) } - val out = - if (armor) { - ArmoredOutputStreamFactory.get(outputStream) - } else { - outputStream - } - - // emit merged and updated base certs - for (merged in baseCerts.values) { - out.write(merged.getEncoded()) - } - if (armor) { - out.close() + OpenPGPCertificateUtil.armor(baseCerts.values, outputStream) + } else { + OpenPGPCertificateUtil.encode(baseCerts.values, outputStream) } - outputStream.close() } } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt index 27717ec0..0c7e3585 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt @@ -14,7 +14,7 @@ import org.pgpainless.bouncycastle.extensions.toOpenPGPCertificate import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.key.util.RevocationAttributes -import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.OpenPGPCertificateUtil import org.pgpainless.util.Passphrase import sop.Ready import sop.exception.SOPGPException @@ -67,15 +67,9 @@ class RevokeKeyImpl(private val api: PGPainless) : RevokeKey { return object : Ready() { override fun writeTo(outputStream: OutputStream) { if (armor) { - val armorOut = ArmoredOutputStreamFactory.get(outputStream) - for (cert in revocationCertificates) { - armorOut.write(cert.getEncoded()) - } - armorOut.close() + OpenPGPCertificateUtil.armor(revocationCertificates, outputStream) } else { - for (cert in revocationCertificates) { - outputStream.write(cert.getEncoded()) - } + OpenPGPCertificateUtil.encode(revocationCertificates, outputStream) } } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/UpdateKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/UpdateKeyImpl.kt index 28f4a296..c9f8e605 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/UpdateKeyImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/UpdateKeyImpl.kt @@ -7,12 +7,10 @@ package org.pgpainless.sop import java.io.InputStream import java.io.OutputStream import org.bouncycastle.bcpg.KeyIdentifier -import org.bouncycastle.bcpg.PacketFormat import org.bouncycastle.openpgp.PGPSecretKeyRing import org.bouncycastle.openpgp.api.OpenPGPCertificate -import org.bouncycastle.openpgp.api.OpenPGPKey import org.pgpainless.PGPainless -import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.OpenPGPCertificateUtil import org.pgpainless.util.Passphrase import sop.Ready import sop.operation.UpdateKey @@ -29,33 +27,25 @@ class UpdateKeyImpl(private val api: PGPainless) : UpdateKey { override fun key(key: InputStream): Ready { return object : Ready() { override fun writeTo(outputStream: OutputStream) { - val out = - if (armor) { - ArmoredOutputStreamFactory.get(outputStream) - } else { - outputStream - } - - val keyList = api.readKey().parseKeys(key) - for (k in keyList) { - val updatedKey: OpenPGPKey = - if (mergeCerts[k.keyIdentifier] == null) { - k + val keyList = + api.readKey().parseKeys(key).map { + if (mergeCerts[it.keyIdentifier] == null) { + it } else { val updatedCert: OpenPGPCertificate = api.mergeCertificate( - k.toCertificate(), mergeCerts[k.keyIdentifier]!!) + it.toCertificate(), mergeCerts[it.keyIdentifier]!!) api.toKey( PGPSecretKeyRing.replacePublicKeys( - k.pgpSecretKeyRing, updatedCert.pgpPublicKeyRing)) + it.pgpSecretKeyRing, updatedCert.pgpPublicKeyRing)) } - out.write(updatedKey.getEncoded(PacketFormat.CURRENT)) - } + } if (armor) { - out.close() + OpenPGPCertificateUtil.armor(keyList, outputStream) + } else { + OpenPGPCertificateUtil.encode(keyList, outputStream) } - outputStream.close() } } }