diff --git a/src/main/java/com/cloudbees/plugins/credentials/CredentialsSelectHelper.java b/src/main/java/com/cloudbees/plugins/credentials/CredentialsSelectHelper.java index e07df340..04ddfe5b 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/CredentialsSelectHelper.java +++ b/src/main/java/com/cloudbees/plugins/credentials/CredentialsSelectHelper.java @@ -39,6 +39,7 @@ import hudson.security.AccessControlled; import hudson.security.Permission; import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -47,10 +48,14 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; + import jakarta.servlet.ServletException; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.exception.ExceptionUtils; import org.jenkins.ui.icon.IconSpec; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -76,6 +81,8 @@ public class CredentialsSelectHelper extends Descriptor */ public static final Permission CREATE = CredentialsProvider.CREATE; + private static final Logger LOGGER = Logger.getLogger(CredentialsSelectHelper.class.getName()); + /** * {@inheritDoc} */ @@ -608,8 +615,34 @@ public JSONObject doAddCredentials(StaplerRequest2 req, StaplerResponse2 rsp) th .element("notificationType", "ERROR"); } store.checkPermission(CredentialsStoreAction.CREATE); - Credentials credentials = Descriptor.bindJSON(req, Credentials.class, data.getJSONObject("credentials")); - boolean credentialsWereAdded = store.addCredentials(wrapper.getDomain(), credentials); + boolean credentialsWereAdded; + try { + Credentials credentials = Descriptor.bindJSON(req, Credentials.class, + data.getJSONObject("credentials")); + credentialsWereAdded = store.addCredentials(wrapper.getDomain(), credentials); + } catch (LinkageError e) { + /* + * Descriptor#newInstanceImpl throws a LinkageError if the DataBoundConstructor or any DataBoundSetter + * throw any exception other than RuntimeException implementing HttpResponse. + * + * Checked exceptions implementing HttpResponse like FormException are wrapped and + * rethrown as HttpResponseException (a RuntimeException implementing HttpResponse) in + * RequestImpl#invokeConstructor. + * + * This approach is taken to maintain backward compatibility, as throwing a FormException directly + * from the constructor would result in a source-incompatible change, potentially breaking dependent plugins. + * + * Here, known exceptions are caught specifically to provide meaningful error response. + */ + Throwable rootCause = ExceptionUtils.getRootCause(e); + if (rootCause instanceof IOException || rootCause instanceof IllegalArgumentException + || rootCause instanceof GeneralSecurityException) { + LOGGER.log(Level.WARNING, "Failed to create Credentials", e); + return new JSONObject().element("message", rootCause.getMessage()).element("notificationType", + "ERROR"); + } + throw e; + } if (credentialsWereAdded) { return new JSONObject() .element("message", "Credentials created") diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index defcddc7..6b95145f 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -132,6 +132,9 @@ public CertificateCredentialsImpl(@CheckForNull CredentialsScope scope, @NonNull KeyStoreSource keyStoreSource) { super(scope, id, description); Objects.requireNonNull(keyStoreSource); + if (FIPS140.useCompliantAlgorithms() && StringUtils.length(password) < 14) { + throw new IllegalArgumentException(Messages.CertificateCredentialsImpl_ShortPasswordFIPS()); + } this.password = Secret.fromString(password); this.keyStoreSource = keyStoreSource; // ensure the keySore is valid diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java index 34321f8f..f652cdca 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java @@ -42,8 +42,6 @@ import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.interceptor.RequirePOST; -import java.util.Objects; - /** * Concrete implementation of {@link StandardUsernamePasswordCredentials}. * diff --git a/src/main/resources/lib/credentials/select/select.js b/src/main/resources/lib/credentials/select/select.js index 1114fb97..f1ef36a6 100644 --- a/src/main/resources/lib/credentials/select/select.js +++ b/src/main/resources/lib/credentials/select/select.js @@ -123,6 +123,7 @@ window.credentials.addSubmit = function (_) { .catch((e) => { // notificationBar.show(...) with logging ID could be handy here? console.error("Could not add credentials:", e); + window.notificationBar.show("Credentials creation failed", window.notificationBar["ERROR"]); }) } }; diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java index 5703712c..3d8c7ec0 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java @@ -1,18 +1,36 @@ package com.cloudbees.plugins.credentials; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertTrue; import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl; +import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImplTest; + import hudson.model.UnprotectedRootAction; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.List; +import net.sf.json.JSONObject; +import org.apache.commons.io.IOUtils; import org.hamcrest.Matchers; +import org.htmlunit.Page; +import org.htmlunit.html.DomNode; +import org.htmlunit.html.DomNodeList; import org.htmlunit.html.HtmlButton; +import org.htmlunit.html.HtmlElementUtil; import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlFormUtil; import org.htmlunit.html.HtmlInput; +import org.htmlunit.html.HtmlOption; import org.htmlunit.html.HtmlPage; +import org.htmlunit.html.HtmlRadioButtonInput; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestExtension; @@ -20,6 +38,21 @@ public class CredentialsSelectHelperTest { @Rule public JenkinsRule j = new JenkinsRule(); + private String pemCert; + private String pemKey; + + private static final String VALID_PASSWORD = "password"; + private static final String INVALID_PASSWORD = "bla"; + + @Before + public void setup() throws IOException { + pemCert = IOUtils.toString(CertificateCredentialsImplTest.class.getResource("certs.pem"), + StandardCharsets.UTF_8); + pemKey = IOUtils.toString(CertificateCredentialsImplTest.class.getResource("key.pem"), + StandardCharsets.UTF_8); + } + + @Test public void doAddCredentialsFromPopupWorksAsExpected() throws Exception { try (JenkinsRule.WebClient wc = j.createWebClient()) { @@ -56,6 +89,125 @@ public void doAddCredentialsFromPopupWorksAsExpected() throws Exception { } } + @Test + @Issue("JENKINS-74964") + public void doAddCredentialsFromPopupForPEMCertificateKeystore() throws Exception { + + try (JenkinsRule.WebClient wc = j.createWebClient()) { + HtmlPage htmlPage = wc.goTo("credentials-selection"); + HtmlForm form = selectPEMCertificateKeyStore(htmlPage, wc); + form.getTextAreaByName("_.certChain").setTextContent(pemCert); + form.getTextAreaByName("_.privateKey").setTextContent(pemKey); + form.getInputsByName("_.password").forEach(input -> input.setValue(VALID_PASSWORD)); + Page submit = HtmlFormUtil.submit(form); + JSONObject responseJson = JSONObject.fromObject(submit.getWebResponse().getContentAsString()); + assertThat(responseJson.getString("notificationType"), is("SUCCESS")); + } + } + + @Test + @Issue("JENKINS-74964") + public void doAddCredentialsFromPopupForPEMCertificateKeystore_missingKeyStore() throws Exception { + + try (JenkinsRule.WebClient wc = j.createWebClient()) { + HtmlPage htmlPage = wc.goTo("credentials-selection"); + HtmlForm form = selectPEMCertificateKeyStore(htmlPage, wc); + Page submit = HtmlFormUtil.submit(form); + JSONObject responseJson = JSONObject.fromObject(submit.getWebResponse().getContentAsString()); + assertThat(responseJson.getString("notificationType"), is("ERROR")); + } + } + + @Test + @Issue("JENKINS-74964") + public void doAddCredentialsFromPopupForInvalidPEMCertificateKeystore_missingCert() throws Exception { + + try (JenkinsRule.WebClient wc = j.createWebClient()) { + HtmlPage htmlPage = wc.goTo("credentials-selection"); + HtmlForm form = selectPEMCertificateKeyStore(htmlPage, wc); + form.getTextAreaByName("_.certChain").setTextContent(null); + form.getTextAreaByName("_.privateKey").setTextContent(pemKey); + form.getInputsByName("_.password").forEach(input -> input.setValue(VALID_PASSWORD)); + Page submit = HtmlFormUtil.submit(form); + JSONObject responseJson = JSONObject.fromObject(submit.getWebResponse().getContentAsString()); + assertThat(responseJson.getString("notificationType"), is("ERROR")); + } + } + + @Test + @Issue("JENKINS-74964") + public void doAddCredentialsFromPopupForInvalidPEMCertificateKeystore_missingPassword() throws Exception { + + try (JenkinsRule.WebClient wc = j.createWebClient()) { + HtmlPage htmlPage = wc.goTo("credentials-selection"); + HtmlForm form = selectPEMCertificateKeyStore(htmlPage, wc); + form.getTextAreaByName("_.certChain").setTextContent(pemCert); + form.getTextAreaByName("_.privateKey").setTextContent(pemKey); + Page submit = HtmlFormUtil.submit(form); + JSONObject responseJson = JSONObject.fromObject(submit.getWebResponse().getContentAsString()); + assertThat(responseJson.getString("notificationType"), is("ERROR")); + } + } + + @Test + @Issue("JENKINS-74964") + public void doAddCredentialsFromPopupForInvalidPEMCertificateKeystore_invalidPassword() throws Exception { + + try (JenkinsRule.WebClient wc = j.createWebClient()) { + HtmlPage htmlPage = wc.goTo("credentials-selection"); + HtmlForm form = selectPEMCertificateKeyStore(htmlPage, wc); + form.getTextAreaByName("_.certChain").setTextContent(pemCert); + form.getTextAreaByName("_.privateKey").setTextContent(pemKey); + form.getInputsByName("_.password").forEach(input -> input.setValue(INVALID_PASSWORD)); + Page submit = HtmlFormUtil.submit(form); + JSONObject responseJson = JSONObject.fromObject(submit.getWebResponse().getContentAsString()); + assertThat(responseJson.getString("notificationType"), is("ERROR")); + } + } + + private HtmlForm selectPEMCertificateKeyStore(HtmlPage htmlPage, JenkinsRule.WebClient wc) throws IOException { + HtmlButton addCredentialsButton = htmlPage.querySelector(".credentials-add-menu"); + addCredentialsButton.fireEvent("mouseenter"); + addCredentialsButton.click(); + + HtmlButton jenkinsCredentialsOption = htmlPage.querySelector(".jenkins-dropdown__item"); + jenkinsCredentialsOption.click(); + + wc.waitForBackgroundJavaScript(4000); + HtmlForm form = htmlPage.querySelector("#credentials-dialog-form"); + String certificateDisplayName = j.jenkins.getDescriptor(CertificateCredentialsImpl.class).getDisplayName(); + String KeyStoreSourceDisplayName = j.jenkins.getDescriptor( + CertificateCredentialsImpl.PEMEntryKeyStoreSource.class).getDisplayName(); + DomNodeList allOptions = htmlPage.getDocumentElement().querySelectorAll( + "select.dropdownList option"); + boolean optionFound = selectOption(allOptions, certificateDisplayName); + assertTrue("The Certificate option was not found in the credentials type select", optionFound); + List inputs = htmlPage.getDocumentElement().getByXPath( + "//input[contains(@name, 'keyStoreSource') and following-sibling::label[contains(.,'" + + KeyStoreSourceDisplayName + "')]]"); + assertThat("query should return only a singular input", inputs, hasSize(1)); + HtmlElementUtil.click(inputs.get(0)); + wc.waitForBackgroundJavaScript(4000); + return form; + } + + private static boolean selectOption(DomNodeList allOptions, String optionName) { + return allOptions.stream().anyMatch(domNode -> { + if (domNode instanceof HtmlOption option) { + if (option.getVisibleText().equals(optionName)) { + try { + HtmlElementUtil.click(option); + } catch (IOException e) { + throw new RuntimeException(e); + } + return true; + } + } + + return false; + }); + } + @TestExtension public static class CredentialsSelectionAction implements UnprotectedRootAction { @Override diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplFIPSTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplFIPSTest.java new file mode 100644 index 00000000..2e64483d --- /dev/null +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplFIPSTest.java @@ -0,0 +1,86 @@ +package com.cloudbees.plugins.credentials.impl; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.RealJenkinsRule; + +import hudson.ExtensionList; +import hudson.util.FormValidation; + +import com.cloudbees.plugins.credentials.CredentialsScope; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThrows; + +public class CertificateCredentialsImplFIPSTest { + + @Rule + public RealJenkinsRule rule = new RealJenkinsRule().withFIPSEnabled().javaOptions("-Xmx512m"); + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + private String pemCert; + private String pemKey; + private static final String VALID_PASSWORD = "passwordFipsCheck"; + private static final String INVALID_PASSWORD = "invalidPasswordFipsCheck"; + private static final String SHORT_PASSWORD = "password"; + + @Before + public void setup() throws IOException { + pemCert = IOUtils.toString(CertificateCredentialsImplFIPSTest.class.getResource("validCerts.pem"), + StandardCharsets.UTF_8); + pemKey = IOUtils.toString(CertificateCredentialsImplFIPSTest.class.getResource("validKey.pem"), + StandardCharsets.UTF_8); + } + + @Test + public void doCheckPasswordTest() throws Throwable { + rule.then(r -> { + CertificateCredentialsImpl.DescriptorImpl descriptor = ExtensionList.lookupSingleton( + CertificateCredentialsImpl.DescriptorImpl.class); + FormValidation result = descriptor.doCheckPassword(VALID_PASSWORD); + assertThat(result.kind, is(FormValidation.Kind.OK)); + result = descriptor.doCheckPassword(SHORT_PASSWORD); + assertThat(result.kind, is(FormValidation.Kind.ERROR)); + assertThat(result.getMessage(), + is(StringEscapeUtils.escapeHtml4(Messages.CertificateCredentialsImpl_ShortPasswordFIPS()))); + }); + } + + @Test + public void invalidPEMKeyStoreAndPasswordTest() throws Throwable { + CertificateCredentialsImpl.PEMEntryKeyStoreSource storeSource = new CertificateCredentialsImpl.PEMEntryKeyStoreSource( + pemCert, pemKey); + rule.then(r -> { + new CertificateCredentialsImpl(CredentialsScope.GLOBAL, "valid-certificate-and-password-validation", + "Validate the certificate credentials", VALID_PASSWORD, storeSource); + assertThrows(IllegalArgumentException.class, + () -> new CertificateCredentialsImpl(CredentialsScope.GLOBAL, "empty-password-validation", + "Validate the certificate empty password", "", + storeSource)); + assertThrows(IllegalArgumentException.class, + () -> new CertificateCredentialsImpl(CredentialsScope.GLOBAL, "password-length-validation", + "Validate the certificate password length", + SHORT_PASSWORD, storeSource)); + assertThrows(IllegalArgumentException.class, + () -> new CertificateCredentialsImpl(CredentialsScope.GLOBAL, "invalid-password-validation", + "Validate the certificate password", INVALID_PASSWORD, + storeSource)); + assertThrows(IllegalArgumentException.class, + () -> new CertificateCredentialsImpl(CredentialsScope.GLOBAL, "empty-keystore-validation", + "Validate the invalid certificate keyStore", + VALID_PASSWORD, + new CertificateCredentialsImpl.PEMEntryKeyStoreSource( + null, null))); + }); + } +} diff --git a/src/test/resources/com/cloudbees/plugins/credentials/impl/validCerts.pem b/src/test/resources/com/cloudbees/plugins/credentials/impl/validCerts.pem new file mode 100644 index 00000000..b5afc800 --- /dev/null +++ b/src/test/resources/com/cloudbees/plugins/credentials/impl/validCerts.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDUTCCAjmgAwIBAgIUM2p34T12bOzrnyga5GarEMbBI3QwDQYJKoZIhvcNAQEL +BQAwUDELMAkGA1UEBhMCSU4xCzAJBgNVBAgMAlROMREwDwYDVQQHDAhUaXJ1cHB1 +cjEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCAXDTI0MTIxMjA5 +NTgzM1oYDzIxMjQxMTE4MDk1ODMzWjBQMQswCQYDVQQGEwJJTjELMAkGA1UECAwC +VE4xETAPBgNVBAcMCFRpcnVwcHVyMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz +IFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCoI5pI11oR +2nIvBMdViNN9RpztDYZuJ/tDl5JZz3jFHxwUFuneABMIqiySUFS3HK1Kfsz6Lvbz +qZlLriE1cKKk84GBOlkpISaizAMUyYOkZagmVrsf/QrneLQdYjNZFpOjTCduWmsC +3WLXWGA+t2qlT3jmJr+J5yyHBGme27dtab7UgoSdD2o0FFxXCG15NMkWMi+m+3zW +UK9f4wmaY1wIBd3/umsHM1dv+vwqAfgMMp/I4ISTda2sm3txmDzYGBPzvc/bnsCe +zyahE85Fi/otkqxqzDww2a7YhGY0hjcyihpqYffFw7zWo2syylLcFMFJN1nRoQyj +lshfAeLcGYLHAgMBAAGjITAfMB0GA1UdDgQWBBSSmy62eRSGGzkJnFWrcJDoU3Za +4jANBgkqhkiG9w0BAQsFAAOCAQEAW1YhiizwHdxIqf336vqk0Qn8omcg3v/GotjF +hNqFHiCnr2Y1/+7NwqsvpIpjb354atU5nxJ1X1LkIJz/yPztbVbHzqKn/RUztSV6 ++wCOArAZzTfXcgOf/jgqPRQMJxhtUeU01MKcoliBLzCIdYvNe3XdkjMkPUHxK5vt +Yv5hxx+sfZ1T68xCFCgdkXtJKxkBDj9tR56vPOTN7kxpVPKFQzsI644xxID+JRbi +RFwV8GYK8rF6nVw9EQAfi+PuJj+V8DecUshMy6d7heUKWdu5i/+HgVKx46/RZMkj +gwAW4FDt8+qsdvgxYzCTkrkxbREwAtTv6xgPO2BAPZsECG/DSg== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/src/test/resources/com/cloudbees/plugins/credentials/impl/validKey.pem b/src/test/resources/com/cloudbees/plugins/credentials/impl/validKey.pem new file mode 100644 index 00000000..99373ed3 --- /dev/null +++ b/src/test/resources/com/cloudbees/plugins/credentials/impl/validKey.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQHYC8RH2eVm05YRcn +VVJVYgICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEMXZ/JFe75dATa+k +FvUVFzYEggTQsScTfFvsV/4Qc4wT/U+iEidlUm5hrVYEsKLBsa+Bk11CAK7Lsak+ +gNxnYhvrs+xUll8YkFNUjxwaWGHjgrKNM+dZwO1JqKTy2VYK4LRHcY7oM6PJJszl +DMHWDLVZxMmRS0U9hpwdKA3Xt4lC5hOinlVsWR5geYcmeGjUE+SyydsD7Bypk01g +KSVhTqq1eGJq1s+nmVRlWvFLtw2ftLfH49nhhUvlpFtLO34iDjxs9MX93WJCvvqC +OzbjdoBxjaHI4a3TypjZWGTS/z4itSzRjOV7zn0LH0t30Bp0LUjS0Lxtug+i+kIo +4nFl8RM6SMQLIgYmOT4Y7bcyTq2ZkIGju1Z3I0PEsaIkE2EkIR8E8kvvsG6fXBvH +ahS2Zbga+oOuYAD7VSspgIvfIYkBriEoN3RU9QfOsk/If6JY1vHjkUQU+Yv60/ia +OajDnhOa1cks30P2DYOPBP+lpbMsmg5iUU5QEMF2oEH9afZjpbs2ZvXwo9Gh+qgQ +G7kZ2+v++AZgX5XLjXdIlpo+iXb9VQL9he0nEcbm2yCFPliwHd7S1N1pnZsyyI6U +uVE2cqFv44d/kxB67wYft6yVPsExgvEMlHDIRAMyk6AuaKmDoIYpCGaiCQnQCqBG +xgckie+vjD5zA4q5Ttdzg9inizBEwQgSDHxOh9nuZYjobDfjiDQ00A4NZVNJn/6z +JoUa0eaKEepwW0a0NflKSO98llADaJyE4jqaWAimImvp1frO/OldlqLgFXHSrhZg +l9te4Huj/NwnXAC60hq3OK9H1fEfUiFf+PYBUiLioGyJ9Yd3m5NQAxFDjPi2oBi/ ++hdn1x4e+5rOmO6Y6BRec8v+ofLsnfRgLMnuyFi4uo6jbyqN1JC5z05LPsQcCPYX +lv9lD4LazeJobzOzTQGJretFjuPhGeHx+PzbulC7S3sVq7/ddlFLEmmiqFUzynBo +gpxEpPQMgB5fdVrUOsTKThsVveIQw4wVk3+uvl4AiO0yWaP9tD7Iwf2WP8SzpViU +lnb1f6bQeWtk0FxywHcdFonFIwB0qzOoO38W1tqqXBiYTcJre+bBOrprpLisxyZC +IscEJXYHjicft1PCD71Tau5muN/RB1iTXmURKYoybQ23NFlLEunD8q5kQjYAhgIx +57pcrp6dMc3qinyrSW6EK8myUywlozgEHob+azspIA+HDHz5sUNjQ0+oSBJ8VbAy +s9fo0e9ck8skPQNntgrqzSpF+qh0DXiAKKrHads0qloFcXzQ7znn4X7fKjGfE8bu +jFOd8vo9v3aZ7palgOJzYVh1IVfGGRuZXmmJR1e6qay/ZxBVNd6Kg83xRw4/9117 +ByyTdYZaVsKOhgcqLqfoELyxf5lSg3ep+4x7jL5iQHf4u3u5gnjo6bd84a7W9NkG +z2mUH079NCmIHUNPPafHF+SnNOqf93VkltjafutaDhIJ7eZ4rJd6Cuhrqy7bKB4H +P/2VVcRfIV65wY5KXFYEIDprKdHrRzvMhswY5KggQ2dGGpiBOIweruqKyBd6BFDf +aQitGNUqDsw1CpulRxSAl7V+eVBPxfzzXp/JHADMAZ0qfuqNL//DaLNyElh3ozA/ +RbCFEn/Lei1Poh9Eu4qiMJaTmpp8oKe3GqpzIvLrMZ+vXK7HOCpPrL4= +-----END ENCRYPTED PRIVATE KEY----- \ No newline at end of file