Skip to content

Commit e1089fe

Browse files
authored
Merge pull request #152 from jtnord/support-multiple-encodables
allow multple entries in the PEM string
2 parents 22ed618 + 991bac5 commit e1089fe

4 files changed

Lines changed: 216 additions & 17 deletions

File tree

src/main/java/jenkins/bouncycastle/api/PEMEncodable.java

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@
4747
import java.security.interfaces.RSAPrivateCrtKey;
4848
import java.security.spec.InvalidKeySpecException;
4949
import java.security.spec.RSAPublicKeySpec;
50+
import java.util.ArrayList;
5051
import java.util.Arrays;
52+
import java.util.List;
5153
import java.util.logging.Level;
5254
import java.util.logging.Logger;
5355
import java.util.stream.Collectors;
@@ -166,18 +168,46 @@ public static PEMEncodable decode(@NonNull String pem) throws IOException, Unrec
166168
@NonNull
167169
public static PEMEncodable decode(@NonNull String pem, @Nullable final char[] passphrase)
168170
throws IOException, UnrecoverableKeyException {
171+
List<PEMEncodable> pems = decodeAll(pem, passphrase);
172+
if (pems.isEmpty()) {
173+
throw new IOException(
174+
"Could not parse PEM, only key pairs, private keys, public keys and certificates are supported");
175+
}
176+
if (pems.size() > 1) {
177+
throw new IOException("Expected a single PEM entry, but got " + pems.size());
178+
}
179+
return pems.get(0);
180+
}
169181

170-
try (PEMParser parser = new PEMParser(new StringReader(pem))) {
182+
/**
183+
* Creates a list of {@link PEMEncodable}s by decoding PEM formated data from a {@link String}
184+
*
185+
* @param pem {@link String} with the PEM data
186+
* @param passphrase passphrase for the encrypted PEM data. {@code null} if PEM data is not passphrase protected.
187+
* The caller is responsible for zeroing out the char[] after use to ensure the password does not stay in memory, e.g. with
188+
* <code>Arrays.fill(passphrase, (char)0)</code>
189+
* @return a list of {@link PEMEncodable} objects
190+
* @throws IOException launched if a problem exists reading the PEM information
191+
* @throws UnrecoverableKeyException in case PEM is passphrase protected and none or wrong is provided
192+
*/
193+
@NonNull
194+
public static List<PEMEncodable> decodeAll(@NonNull String pem, @Nullable final char[] passphrase)
195+
throws IOException, UnrecoverableKeyException {
196+
List<PEMEncodable> objects = new ArrayList<>();
171197

172-
Object object = parser.readObject();
198+
try (PEMParser parser = new PEMParser(new StringReader(pem))) {
173199

174-
if (object == null) {
175-
throw new IOException(
176-
"Could not parse PEM, only key pairs, private keys, public keys and certificates are supported");
200+
for (Object object = parser.readObject(); object != null; object = parser.readObject()) {
201+
objects.add(convertedPemToPemDecodable(object, passphrase));
177202
}
203+
}
204+
return objects;
205+
}
178206

207+
private static final PEMEncodable convertedPemToPemDecodable(Object object, char[] passphrase)
208+
throws UnrecoverableKeyException, IOException {
209+
try {
179210
JcaPEMKeyConverter kConv = new JcaPEMKeyConverter().setProvider(BOUNCY_CASTLE_PROVIDER);
180-
181211
// handle supported PEM formats.
182212
if (object instanceof PEMEncryptedKeyPair) {
183213
if (passphrase != null) {
@@ -284,6 +314,36 @@ public static PEMEncodable read(@NonNull File pemFile, @Nullable char[] passphra
284314
return decode(FileUtils.readFileToString(pemFile, StandardCharsets.UTF_8), passphrase);
285315
}
286316

317+
/**
318+
* Creates {@link PEMEncodable}s by reading a PEM file
319+
*
320+
* @param pemFile {@link File} pointing to the PEM file to read
321+
* @return A list of {@link PEMEncodable} objects.
322+
* @throws IOException launched if a problem exists reading the PEM information or the {@link File}
323+
* @throws UnrecoverableKeyException in case PEM is passphrase protected
324+
*/
325+
@NonNull
326+
public static List<PEMEncodable> readAll(@NonNull File pemFile) throws IOException, UnrecoverableKeyException {
327+
return readAll(pemFile, null);
328+
}
329+
330+
/**
331+
* Creates a {@link PEMEncodable}s by reading a PEM file
332+
*
333+
* @param pemFile {@link File} pointing to the PEM file to read
334+
* @param passphrase passphrase for the encrypted PEM data. {@code null} if PEM data is not passphrase protected.
335+
* The caller is responsible for zeroing out the char[] after use to ensure the password does not stay in memory, e.g. with
336+
* <code>Arrays.fill(passphrase, (char)0)</code>
337+
* @return a list of {@link PEMEncodable} objects
338+
* @throws IOException launched if a problem exists reading the PEM information or the {@link File}
339+
* @throws UnrecoverableKeyException in case PEM is passphrase protected and none or wrong is provided
340+
*/
341+
@NonNull
342+
public static List<PEMEncodable> readAll(@NonNull File pemFile, @Nullable char[] passphrase)
343+
throws IOException, UnrecoverableKeyException {
344+
return decodeAll(FileUtils.readFileToString(pemFile, StandardCharsets.UTF_8), passphrase);
345+
}
346+
287347
/**
288348
* Writes the current stored information in PEM formated {@link File}
289349
*

src/test/java/jenkins/bouncycastle/EncodingDecodingTest.java

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@
3535
import java.io.IOException;
3636
import java.net.URISyntaxException;
3737
import java.nio.charset.StandardCharsets;
38+
import java.security.KeyPair;
3839
import java.security.PublicKey;
3940
import java.security.Security;
4041
import java.security.cert.Certificate;
4142
import java.util.Arrays;
43+
import java.util.List;
4244
import jenkins.bouncycastle.api.PEMEncodable;
4345
import org.apache.commons.io.FileUtils;
4446
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -73,6 +75,9 @@ public static void cleanupProvider() {
7375
private static File CERTIFICATE_PUBLIC_KEY_PEM;
7476
private static File CERTIFICATE_PW_PEM;
7577
private static File CERTIFICATE_PUBLIC_KEY_PW_PEM;
78+
private static File CERTIFICATE_AND_PRIVATE_KEY_PEM;
79+
private static File CERTIFICATE_AND_PRIVATE_KEY_PW_PEM;
80+
7681
private static File PRIVATE_KEY_PW_PKCS8;
7782

7883
private static final String PRIVATE_KEY_PW = "test";
@@ -90,6 +95,8 @@ public static void setUpClass() throws URISyntaxException {
9095
CERTIFICATE_PUBLIC_KEY_PEM = getResourceFile("test_cert_key.pem");
9196
CERTIFICATE_PW_PEM = getResourceFile("test_cert_cert_pass.pem");
9297
CERTIFICATE_PUBLIC_KEY_PW_PEM = getResourceFile("test_cert_key_pass.pem");
98+
CERTIFICATE_AND_PRIVATE_KEY_PEM = getResourceFile("test_cert_and_key.pem");
99+
CERTIFICATE_AND_PRIVATE_KEY_PW_PEM = getResourceFile("test_cert_and_key_pass.pem");
93100
}
94101

95102
private static File getResourceFile(String resource) throws URISyntaxException {
@@ -128,7 +135,7 @@ public void testReadPrivateKeyWithPasswordPEM() throws Exception {
128135
@Test
129136
@Issue(value = "JENKINS-66394")
130137
public void testReadPrivateKeyWithPasswordPKCS8() throws Exception {
131-
PEMEncodable pemEnc = PEMEncodable.read(PRIVATE_KEY_PW_PKCS8, "test".toCharArray());
138+
PEMEncodable pemEnc = PEMEncodable.read(PRIVATE_KEY_PW_PKCS8, PRIVATE_KEY_PW.toCharArray());
132139

133140
assertEquals(
134141
new String(Base64.encode(pemEnc.toKeyPair().getPrivate().getEncoded()), StandardCharsets.UTF_8),
@@ -186,11 +193,7 @@ public void testReadCertificatePEM() throws Exception {
186193

187194
Certificate certificate = pemEncCer.toCertificate();
188195
PublicKey publicKey = pemEncKey.toPublicKey();
189-
assertNotNull(certificate);
190-
assertNotNull(publicKey);
191-
assertEquals(
192-
new String(Base64.encode(certificate.getPublicKey().getEncoded()), StandardCharsets.UTF_8),
193-
new String(Base64.encode(publicKey.getEncoded()), StandardCharsets.UTF_8));
196+
assertCertificatePublicKeyMatches(certificate, publicKey);
194197
}
195198

196199
@Test
@@ -200,11 +203,7 @@ public void testReadCertificateWithPasswordPEM() throws Exception {
200203

201204
Certificate certificate = pemEncCer.toCertificate();
202205
PublicKey publicKey = pemEncKey.toPublicKey();
203-
assertNotNull(certificate);
204-
assertNotNull(publicKey);
205-
assertEquals(
206-
new String(Base64.encode(certificate.getPublicKey().getEncoded()), StandardCharsets.UTF_8),
207-
new String(Base64.encode(publicKey.getEncoded()), StandardCharsets.UTF_8));
206+
assertCertificatePublicKeyMatches(certificate, publicKey);
208207
}
209208

210209
@Test
@@ -271,4 +270,37 @@ public void testReadKeyPairFromPCKS8PEM() throws Exception {
271270
public void testInvalidPEM() throws Exception {
272271
PEMEncodable.decode(FileUtils.readFileToString(getResourceFile("invalid.pem"), StandardCharsets.UTF_8));
273272
}
273+
274+
@Test
275+
public void testReadingCertAndKeyPEM() throws Exception {
276+
List<PEMEncodable> pems = PEMEncodable.readAll(CERTIFICATE_AND_PRIVATE_KEY_PEM);
277+
assertThat(pems).hasSize(2);
278+
assertCertPublicKeyMatches(pems.get(0).toCertificate(), pems.get(1).toKeyPair());
279+
}
280+
281+
@Test
282+
public void testReadingCertAndKeyPassPEM() throws Exception {
283+
List<PEMEncodable> pems =
284+
PEMEncodable.readAll(CERTIFICATE_AND_PRIVATE_KEY_PW_PEM, PRIVATE_KEY_PW.toCharArray());
285+
assertThat(pems).hasSize(2);
286+
assertCertPublicKeyMatches(pems.get(0).toCertificate(), pems.get(1).toKeyPair());
287+
}
288+
289+
/**
290+
* asserts that the given certificates public key corresponds to the provided KeyPair.
291+
*/
292+
private static void assertCertPublicKeyMatches(Certificate cert, KeyPair kp) {
293+
assertCertificatePublicKeyMatches(cert, kp != null ? kp.getPublic() : null);
294+
}
295+
296+
/**
297+
* asserts that the given certificates public key corresponds to the provided KeyPair.
298+
*/
299+
private static void assertCertificatePublicKeyMatches(Certificate cert, PublicKey key) {
300+
assertNotNull(cert);
301+
assertNotNull(key);
302+
assertEquals(
303+
new String(Base64.encode(cert.getPublicKey().getEncoded()), StandardCharsets.UTF_8),
304+
new String(Base64.encode(key.getEncoded()), StandardCharsets.UTF_8));
305+
}
274306
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIERDCCAyygAwIBAgIJALRTbcJ8ZNFQMA0GCSqGSIb3DQEBBQUAMHIxCzAJBgNV
3+
BAYTAlVTMQ0wCwYDVQQIEwR0ZXN0MQ0wCwYDVQQHEwR0ZXN0MQ0wCwYDVQQKEwR0
4+
ZXN0MQ0wCwYDVQQLEwR0ZXN0MQ0wCwYDVQQDEwR0ZXN0MRgwFgYJKoZIhvcNAQkB
5+
Fgl0ZXN0QHRlc3QwIxcNMTYwNjA4MDk0ODE2WhgSNDI5ODY4MjA3MDIwOTQ4MTZa
6+
MHIxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIEwR0ZXN0MQ0wCwYDVQQHEwR0ZXN0MQ0w
7+
CwYDVQQKEwR0ZXN0MQ0wCwYDVQQLEwR0ZXN0MQ0wCwYDVQQDEwR0ZXN0MRgwFgYJ
8+
KoZIhvcNAQkBFgl0ZXN0QHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
9+
AoIBAQCcmTOLsanEwTJ+ImFzV2OQs72OninuI0K2Gz19144zxPo3R1VYKXaJ0Ian
10+
I+jVidk3l0BB2MN+hN7V2AXNw7a3Hsf52zjIZwCo9NtHbL5VCsVdvJtcmC2zefXf
11+
A5NsI+k/qtuN9QRTGHQasVQhPyK9FvuXWKE49B1xr224JBPVpDwM/Jx4LLgVDigN
12+
Fd035Fupe6I4Yy1Qxq6E1HMB8eIfoYMkXHL0Xg7j0UzCt2eE4Ja9fRVkMYF15pyQ
13+
8Un6JQcIT7i+NvImANNDepgSicmIIDYZFCNCo5FjXkqPycIvEjHIqIH16A6r7xJT
14+
yLbMlMGks8m9C0Rjol/vWGYP/13XAgMBAAGjgdcwgdQwHQYDVR0OBBYEFPJ38ls+
15+
gUgWb2+h3XxQxPNZO395MIGkBgNVHSMEgZwwgZmAFPJ38ls+gUgWb2+h3XxQxPNZ
16+
O395oXakdDByMQswCQYDVQQGEwJVUzENMAsGA1UECBMEdGVzdDENMAsGA1UEBxME
17+
dGVzdDENMAsGA1UEChMEdGVzdDENMAsGA1UECxMEdGVzdDENMAsGA1UEAxMEdGVz
18+
dDEYMBYGCSqGSIb3DQEJARYJdGVzdEB0ZXN0ggkAtFNtwnxk0VAwDAYDVR0TBAUw
19+
AwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAAw6a/5v3IJNidNaK3hgSrCNrh56yQlA9
20+
NaNNwVQZFy5a09yxz0jS8NksNQhnnzZcMFCOcHFksg0USO202Fg/lsYe1DTAuowv
21+
obSDu4L95eHrJCYgsVrzvGOIKpwA30ANt35Z82SCCcxMFlqjd1wrMQN2nzlDYIzV
22+
TjjlRZ+Bwb4NMiKziCadm61nfBsa3ufhXpA5xA2Ds/CwJOAQZ4pY9MPCTOFtKMXt
23+
ZP/aXpv4tqm0V+8tj2LAGg1GeTZNwLt8oOAuPLsbDPvYERbYqD9JflOJ6/wJsiDl
24+
OTbki00s67/n8aWPf1q1W9qpbJfCGyKaDaEvwTQSi2xReZpM9NMMrA==
25+
-----END CERTIFICATE-----
26+
-----BEGIN RSA PRIVATE KEY-----
27+
MIIEogIBAAKCAQEAnJkzi7GpxMEyfiJhc1djkLO9jp4p7iNCths9fdeOM8T6N0dV
28+
WCl2idCGpyPo1YnZN5dAQdjDfoTe1dgFzcO2tx7H+ds4yGcAqPTbR2y+VQrFXbyb
29+
XJgts3n13wOTbCPpP6rbjfUEUxh0GrFUIT8ivRb7l1ihOPQdca9tuCQT1aQ8DPyc
30+
eCy4FQ4oDRXdN+RbqXuiOGMtUMauhNRzAfHiH6GDJFxy9F4O49FMwrdnhOCWvX0V
31+
ZDGBdeackPFJ+iUHCE+4vjbyJgDTQ3qYEonJiCA2GRQjQqORY15Kj8nCLxIxyKiB
32+
9egOq+8SU8i2zJTBpLPJvQtEY6Jf71hmD/9d1wIDAQABAoIBAA5QGlv8Qw4OsnTA
33+
47NNG3aQVxT/88kmQ+StHLOxzfeBW3VN+cjzETiLbKM+LvvFe6sPEfpyu3uG3W8i
34+
LXWfG8Yk9BYsKUZ306Gdr6UZlVPkhiGJKZIO/q5WyVg21XSe26TMEwEokdqec441
35+
AwF+6cfqn9g3a5TvCvnnFNotwfpWwd6WWlTHFFkDOGG1tT2T6bARwhyHNQjBnF4x
36+
8ZDgo38kX4wzJvin8S8TfVShD21EmpCvwpVnLw8hlS0UNCo+tVkYl7PhPaXoSNQZ
37+
x961sR4Ch043083444P+Xa2f7O97XQ5YpSceFVaGHck3ekCqYEHd7WoV8zET4JNI
38+
oqpCt2ECgYEAz3uNKATuOv5WWqAHaN5qjthEXNPZ3o7CMt0FhmyXiYKhvt2pYlrs
39+
H2Uk3PGmmmjaTxxjAfiFjx3y9wlWs5XHllNkWsiT1MxfTedjzLo3VXUpZpKPdHZw
40+
1yrYrp3RwQIPbWe30PEeBQ5r1/K9Tpz8728v+rN/FZvPGZgf2GFcEE8CgYEAwTeX
41+
P1JGj4wuIHf+WCxLlswcbIdExlcPSlghrMiLTsr9ma9Ku/yO1Uuzg452mbl4OeXl
42+
XlVJdXMw62uuBdVToMCh0t7GWdFiqi7uiVY0M88ciOP6+vcTzpyefyPnUBhl+0Q7
43+
rNctgwzpL/rC8jkjtJ/A3oICmPup1XaJ7bf7L/kCgYBFe0+jFjudX+0VPkAc+1bd
44+
7Ui/eWadpFseY2vlP1pj/24hF+QQfvhR7zIwF3C+htyM36m8UDiR2+qwld0GGOOU
45+
0OpriH4RJjvIP/Po7Hd5U9oAcpY93ygctkmH+Gv04ba+4ZvqCcUO1myBDdYdjG8b
46+
07VSlGe7mcj5/jqz4K7O+wKBgB+a2EKjUqdoZqMo/ZGurS6ddIjMF5PZ6cZfZCK/
47+
KwpYaeMZqT/WsAZzu4lpZD0A+Yl+8WXczYdIsFfR5UVO/77rw4yapBdNCLaNtrcA
48+
Qsm2txQtNoCWqcbCF6r0VIHle8j+AETlbaxiEEKo4fp9HjoNZ5795S7g+4bPPQCF
49+
d7EBAoGAFXG9MZGC+ZPxY13lFIm0PFRdcF9Z7kxiFIXydHavV40Yl9M70mr6OGVs
50+
Zb5PUsDs1Og4T5OdlNNgW7W2bO6jcBDoa0j2xgV8nkTfMhgXYS75ANPZxlEU9WN6
51+
vsIjOkD3bFZClImXIyLn6CAaftF41vt/vnTwD1kJNg2kGWKyW9I=
52+
-----END RSA PRIVATE KEY-----
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIERDCCAyygAwIBAgIJALRTbcJ8ZNFQMA0GCSqGSIb3DQEBBQUAMHIxCzAJBgNV
3+
BAYTAlVTMQ0wCwYDVQQIEwR0ZXN0MQ0wCwYDVQQHEwR0ZXN0MQ0wCwYDVQQKEwR0
4+
ZXN0MQ0wCwYDVQQLEwR0ZXN0MQ0wCwYDVQQDEwR0ZXN0MRgwFgYJKoZIhvcNAQkB
5+
Fgl0ZXN0QHRlc3QwIxcNMTYwNjA4MDk0ODE2WhgSNDI5ODY4MjA3MDIwOTQ4MTZa
6+
MHIxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIEwR0ZXN0MQ0wCwYDVQQHEwR0ZXN0MQ0w
7+
CwYDVQQKEwR0ZXN0MQ0wCwYDVQQLEwR0ZXN0MQ0wCwYDVQQDEwR0ZXN0MRgwFgYJ
8+
KoZIhvcNAQkBFgl0ZXN0QHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
9+
AoIBAQCcmTOLsanEwTJ+ImFzV2OQs72OninuI0K2Gz19144zxPo3R1VYKXaJ0Ian
10+
I+jVidk3l0BB2MN+hN7V2AXNw7a3Hsf52zjIZwCo9NtHbL5VCsVdvJtcmC2zefXf
11+
A5NsI+k/qtuN9QRTGHQasVQhPyK9FvuXWKE49B1xr224JBPVpDwM/Jx4LLgVDigN
12+
Fd035Fupe6I4Yy1Qxq6E1HMB8eIfoYMkXHL0Xg7j0UzCt2eE4Ja9fRVkMYF15pyQ
13+
8Un6JQcIT7i+NvImANNDepgSicmIIDYZFCNCo5FjXkqPycIvEjHIqIH16A6r7xJT
14+
yLbMlMGks8m9C0Rjol/vWGYP/13XAgMBAAGjgdcwgdQwHQYDVR0OBBYEFPJ38ls+
15+
gUgWb2+h3XxQxPNZO395MIGkBgNVHSMEgZwwgZmAFPJ38ls+gUgWb2+h3XxQxPNZ
16+
O395oXakdDByMQswCQYDVQQGEwJVUzENMAsGA1UECBMEdGVzdDENMAsGA1UEBxME
17+
dGVzdDENMAsGA1UEChMEdGVzdDENMAsGA1UECxMEdGVzdDENMAsGA1UEAxMEdGVz
18+
dDEYMBYGCSqGSIb3DQEJARYJdGVzdEB0ZXN0ggkAtFNtwnxk0VAwDAYDVR0TBAUw
19+
AwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAAw6a/5v3IJNidNaK3hgSrCNrh56yQlA9
20+
NaNNwVQZFy5a09yxz0jS8NksNQhnnzZcMFCOcHFksg0USO202Fg/lsYe1DTAuowv
21+
obSDu4L95eHrJCYgsVrzvGOIKpwA30ANt35Z82SCCcxMFlqjd1wrMQN2nzlDYIzV
22+
TjjlRZ+Bwb4NMiKziCadm61nfBsa3ufhXpA5xA2Ds/CwJOAQZ4pY9MPCTOFtKMXt
23+
ZP/aXpv4tqm0V+8tj2LAGg1GeTZNwLt8oOAuPLsbDPvYERbYqD9JflOJ6/wJsiDl
24+
OTbki00s67/n8aWPf1q1W9qpbJfCGyKaDaEvwTQSi2xReZpM9NMMrA==
25+
-----END CERTIFICATE-----
26+
-----BEGIN ENCRYPTED PRIVATE KEY-----
27+
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIlc860VZi3CICAggA
28+
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBC4VJ/V/tPWSifNEt9IBtaGBIIE
29+
0AfC9EzpJmTyWm13arpi/4ChEKoee9YCx47NvsREJTKXdwLBY/RDoMoFBn4cCXEJ
30+
LdSC/m/tvaxRCWMFNK1F36jYIJ/jU/3/zyVHQUbPM00B5SMBCk1priv+tiNtUSVm
31+
tn9IJNb007vnQkAE+UawIaAmZUakDAyEmRKZNv3Oq9cy6iuVAnp4CLFHPrtnR0AI
32+
lkDwqPUTS1pKxtEAjQ82Scr/eUVd8b/kHmJ5tnSzwB3BfxuQzT+puuLdpbG/xElH
33+
izI5GqXfcw2YiHDUFB7WHC9m4SqX1j7AKGs0pJx+KS5bTWsPvnqj8ENjOhS+LN6E
34+
qbC17DEZJR4R6nNUbwWY9CJNl5Y03AbvCpwoYb8nfwFYHjgEv82Ud2Oq8ECUa1gv
35+
xEaIRLzOlpiI//uLbtz/MgTafs6bDPgO0dZ4uSdhAid1KFTPb34V1OXQRZkwicSE
36+
gl/tchVGqYIdhx4JiTUS2eFFsZtNHGDGP6YXCldLPwhbGIPADcR3O7Uf1pzBJRME
37+
7JfbENU0pIXad89uipXqbe5lZwJuxEhnsBbX98un3bO8GkNA+/Lj3WrhlPBxDXIX
38+
V1GjnJl84U+jqzPzHyGHq+GYOaPVh7CSuv80VX9DlXuskVr1VrNRSxWOmzjA+kyL
39+
cG+osB1ftx0ov1D1TC8rmxKpRWwwmVG3+fUuGVmLxofS04Amqj0L9tqXtKINtY89
40+
bEmaNaHsjQESnZPoESxHqFJ+3p4wD0BxR8dt2/UXGDwyTLO4WjvxU4qzOtq5Il0U
41+
bN//K+ZdP0O0EQ4qdgXpBGR8mfC/iXJtORjw6OnpXkzBNx7bwQtois0MCBunZoGA
42+
nGTkeHlkP4VAkQ71JNINVmp2LgjJaBDzY2nFaq01nAbgrz+nhbDnVjkNxxQjTQNn
43+
V645LokxT5JH9lzlmSF63+Y6rSJY8weVdjpM7sq2u2jOLDrCeZ3nCtXWxTJtxgit
44+
GLJHZ8yazMRbAC1tFui1HFlZjs8w/uNKKeQIStweOzw4foqVwBQF8oyz9qvFr5PA
45+
/YE7ODp7mzAB0H7by2i3LtQ6UeJhpzIcb/TcQ7apmSDCY2n2ZytcWHzsUDCPmmth
46+
rxkeuLbjYXmkfiwul3k6VdAS7P4RkojotoE9jXxrVefPrCXR5zWqATITlMqgzheq
47+
fvrriO3FNgmMbfdH6CZyjlv0ABoCzvRhQA0h9xNTuEGT42IhhrFxA0gUAmOnKKJN
48+
gbCQtIZj+5+H9klBgqS1EiXwwhAr3RL9/NNPHMEUOD6vpv/s+b9pJb7RpjgQIuTA
49+
IbWvHGQGRcjV11coSupm6BJClSS00eeqSGJmHw+AbLIgmA6B4iHOPRCrLNNsT0i1
50+
nT4+ujppmLkHmmvLY/3B/hrOyXMJJ/4uA5FF2ecOcOWtTZF8bSWBZotDNLNDnLgF
51+
hgobmnXK/L8/DBfNR/D+gtvMkYtscSR7/1qxD2MKrcwsiZu6W7JFNiG5uASxtA0E
52+
c8Js2n0kPCWLgXfFwj9JoatQYc9muCfTg6HvoL0j7VvPq3SB0tcpumMfcWAkMzB7
53+
vNJb+LGi74NKJFWl2EFgmS4mSdceZzAKKxji3REP96X0IgXea+5trU48l1FwLKMD
54+
g+H3fhDz1uWE/McsDZbA5VPl6s54O8pJmL27xhhW2Weh
55+
-----END ENCRYPTED PRIVATE KEY-----

0 commit comments

Comments
 (0)