CVE-2017-2598 (Java, published 2018-05-23T13:29Z)
Description: value: Jenkins before versions 2.44, 2.32.2 uses AES ECB block cipher mode without IV for encrypting secrets which makes Jenkins and the stored secrets vulnerable to unnecessary risks (SECURITY-304).
Severity: MEDIUM (CVSS v3 base score: 4.3)
Weakness: CWE-326 \u2014 Inadequate Encryption Strength
The product stores or transmits sensitive data using an encryption scheme that is theoretically sound, but is not strong enough for the level of protection required.
Repository: https://github.com/jenkinsci/jenkins
Files changed:
core/src/main/java/hudson/model/Item.java
core/src/main/java/hudson/util/HistoricalSecrets.java
core/src/main/java/hudson/util/Secret.java
core/src/main/java/hudson/util/SecretRewriter.java
core/src/main/java/jenkins/security/CryptoConfidentialKey.java
core/src/test/groovy/hudson/util/SecretRewriterTest.groovy
core/src/test/groovy/hudson/util/SecretTest.groovy
test/src/test/java/hudson/util/SecretCompatTest.java
test/src/test/java/jenkins/security/RekeySecretAdminMonitorTest.java
test/src/test/java/lib/form/PasswordTest.java
test/src/test/resources/hudson/util/SecretCompatTest/canReadPreSec304Secrets/config.xml
test/src/test/resources/hudson/util/SecretCompatTest/canReadPreSec304Secrets/jobs/OldSecret/config.xml
test/src/test/resources/hudson/util/SecretCompatTest/canReadPreSec304Secrets/secrets/master.key
Commit message: Merge pull request #105 from jenkinsci-cert/SECURITY-304-t3

[SECURITY-304] Encrypt new secrets with CBC and random IV instead of ECB

Vulnerable code:
     * If the user lacks {@link CONFIGURE} then any {@link Secret}s must be masked out, even in encrypted form.
import javax.crypto.SecretKey;
    private Secret(String value) {
    /**
     * Turns {@link Jenkins#getSecretKey()} into an AES key.
     *
     * @deprecated
     * This is no longer the key we use to encrypt new information, but we still need this
     * to be able to decrypt what's already persisted.
     */
    @Deprecated
    /*package*/ static SecretKey getLegacyKey() throws GeneralSecurityException {
        String secret = SECRET;
        if(secret==null)    return Jenkins.getInstance().getSecretKeyAsAES128();
        return Util.toAes128Key(secret);
    }

            Cipher cipher = KEY.encrypt();
            // add the magic suffix which works like a check sum.
            return new String(Base64.encode(cipher.doFinal((value+MAGIC).getBytes("UTF-8"))));
        } catch (UnsupportedEncodingException e) {
            throw new Error(e); // impossible
     * Pattern matching a possible output of {@link #getEncryptedValue}.
     * Basically, any Base64-encoded value.
     * You must then call {@link #decrypt} to eliminate false positives.
    public static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("[A-Za-z0-9+/]+={0,2}");
        if(data==null)      return null;
        try {
            byte[] in = Base64.decode(data.toCharArray());
            Secret s = tryDecrypt(KEY.decrypt(), in);
            if (s!=null)    return s;
            // try our historical key for backward compatibility
            Cipher cipher = getCipher("AES");
            cipher.init(Cipher.DECRYPT_MODE, getLegacyKey());
            return tryDecrypt(cipher, in);
        } catch (GeneralSecurityException e) {
            return null;
        } catch (UnsupportedEncodingException e) {
            throw new Error(e); // impossible
        } catch (IOException e) {
            return null;
        }
    }

    /*package*/ static Secret tryDecrypt(Cipher cipher, byte[] in) throws UnsupportedEncodingException {
        try {
            String plainText = new String(cipher.doFinal(in), "UTF-8");
            if(plainText.endsWith(MAGIC))
                return new Secret(plainText.substring(0,plainText.length()-MAGIC.length()));
            return null;
        } catch (GeneralSecurityException e) {
            return null; // if the key doesn't match with the bytes, it can result in BadPaddingException
    private static final String MAGIC = "::::MAGIC::::";

        key = Secret.getLegacyKey();
        Secret sec = Secret.tryDecrypt(cipher, in);
                        secret = new SecretKeySpec(payload,0,128/8, ALGORITHM);
            Cipher cipher = Secret.getCipher(ALGORITHM);
            Cipher cipher = Secret.getCipher(ALGORITHM);
    private static final String ALGORITHM = "AES";
                  "<foo>${n}</foo>"
                  "<foo>${n}</foo><foo>${n}</foo>"
                  "<foo>${n}</foo>"
                  "  <foo>thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret</foo>  "
        roundtrip "<foo>$o", "<foo>$o"
        roundtrip "$o</foo>", "$o</foo>"
        roundtrip "<abc>\n<foo>$o</foo>\n</abc>", "<abc>\n<foo>$n</foo>\n</abc>"
    void roundtrip(String before, String after) {
        assert after.replaceAll(System.getProperty("line.separator"), "\n").trim()==f.text.replaceAll(System.getProperty("line.separator"), "\n").trim()
        cipher.init(Cipher.ENCRYPT_MODE, Secret.legacyKey);
        return new String(Base64.encode(cipher.doFinal((str + Secret.MAGIC).getBytes("UTF-8"))))
            assert new File(t,"$p/foo.xml").text.trim()==answer
        assert new File(t2,"foo.xml").text.trim()==answer.trim();
import javax.crypto.Cipher;
            assert Secret.ENCRYPTED_VALUE_PATTERN.matcher(ciphertext).matches();
        assert !Secret.ENCRYPTED_VALUE_PATTERN.matcher("hello world").matches();
        assert xml.contains(s.encryptedValue)
        def legacy = Secret.legacyKey
            def old = new String(Base64.encode(cipher.doFinal((str + Secret.MAGIC).getBytes("UTF-8"))))
        assertEquals("<foo>" + encryptNew(TEST_KEY) + "</foo>".trim(),
                FileUtils.readFileToString(xml).trim());
    @Issue("SECURITY-266")
            String sEnc = s.getEncryptedValue();
            assertThat(configure.getWebResponse().getContentAsString(), containsString(sEnc));
            assertThat(xmlAdmin, containsString("<secret>" + sEnc + "</secret>"));
            assertThat(configure.getWebResponse().getContentAsString(), not(containsString(sEnc)));
            assertThat(xmlDev, not(containsString(sEnc)));
            assertEquals(xmlAdmin.replace(sEnc, "********"), xmlDev);

Fixed code:
     * If the user lacks {@link #CONFIGURE} then any {@link Secret}s must be masked out, even in encrypted form.
/*
 * The MIT License
 *
 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
 * Copyright (c) 2016, CloudBees Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package hudson.util;

import com.trilead.ssh2.crypto.Base64;
import hudson.Util;
import jenkins.model.Jenkins;
import jenkins.security.CryptoConfidentialKey;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.security.GeneralSecurityException;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Historical algorithms for decrypting {@link Secret}s.
 */
@Restricted(NoExternalUse.class)
public class HistoricalSecrets {

    /*package*/ static Secret decrypt(String data, CryptoConfidentialKey key) throws IOException, GeneralSecurityException {
        byte[] in = Base64.decode(data.toCharArray());
        Secret s = tryDecrypt(key.decrypt(), in);
        if (s!=null)    return s;

        // try our historical key for backward compatibility
        Cipher cipher = Secret.getCipher("AES");
        cipher.init(Cipher.DECRYPT_MODE, getLegacyKey());
        return tryDecrypt(cipher, in);
    }

    /*package*/ static Secret tryDecrypt(Cipher cipher, byte[] in) {
        try {
            String plainText = new String(cipher.doFinal(in), UTF_8);
            if(plainText.endsWith(MAGIC))
                return new Secret(plainText.substring(0,plainText.length()-MAGIC.length()));
            return null;
        } catch (GeneralSecurityException e) {
            return null; // if the key doesn't match with the bytes, it can result in BadPaddingException
        }
    }

    /**
     * Turns {@link Jenkins#getSecretKey()} into an AES key.
     *
     * @deprecated
     * This is no longer the key we use to encrypt new information, but we still need this
     * to be able to decrypt what's already persisted.
     */
    @Deprecated
    /*package*/ static SecretKey getLegacyKey() throws GeneralSecurityException {
        String secret = Secret.SECRET;
        if(secret==null)    return Jenkins.getInstance().getSecretKeyAsAES128();
        return Util.toAes128Key(secret);
    }

    private static final String MAGIC = "::::MAGIC::::";
}
 * Copyright (c) 2016, CloudBees Inc.
import java.util.Arrays;
import static java.nio.charset.StandardCharsets.UTF_8;

    private static final byte PAYLOAD_V1 = 1;
    private byte[] iv;

    /*package*/ Secret(String value) {
        this.value = value;
    }
    /*package*/ Secret(String value, byte[] iv) {
        this.iv = iv;
            synchronized (this) {
                if (iv == null) { //if we were created from plain text or other reason without iv
                    iv = KEY.newIv();
                }
            }
            Cipher cipher = KEY.encrypt(iv);
            byte[] encrypted = cipher.doFinal(this.value.getBytes(UTF_8));
            byte[] payload = new byte[1 + 8 + iv.length + encrypted.length];
            int pos = 0;
            // For PAYLOAD_V1 we use this byte shifting model, V2 probably will need DataOutput
            payload[pos++] = PAYLOAD_V1;
            payload[pos++] = (byte)(iv.length >> 24);
            payload[pos++] = (byte)(iv.length >> 16);
            payload[pos++] = (byte)(iv.length >> 8);
            payload[pos++] = (byte)(iv.length);
            payload[pos++] = (byte)(encrypted.length >> 24);
            payload[pos++] = (byte)(encrypted.length >> 16);
            payload[pos++] = (byte)(encrypted.length >> 8);
            payload[pos++] = (byte)(encrypted.length);
            System.arraycopy(iv, 0, payload, pos, iv.length);
            pos+=iv.length;
            System.arraycopy(encrypted, 0, payload, pos, encrypted.length);
            return "{"+new String(Base64.encode(payload))+"}";
     * Pattern matching a possible output of {@link #getEncryptedValue}
     * Basically, any Base64-encoded value optionally wrapped by {@code {}}.
     * You must then call {@link #decrypt(String)} to eliminate false positives.
     * @see #ENCRYPTED_VALUE_PATTERN
    public static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("\\{?[A-Za-z0-9+/]+={0,2}}?");
        if (data == null) return null;
        if (data.startsWith("{") && data.endsWith("}")) { //likely CBC encrypted/containing metadata but could be plain text
            byte[] payload;
            try {
                payload = Base64.decode(data.substring(1, data.length()-1).toCharArray());
            } catch (IOException e) {
                return null;
            }
            switch (payload[0]) {
                case PAYLOAD_V1:
                    // For PAYLOAD_V1 we use this byte shifting model, V2 probably will need DataOutput
                    int ivLength = ((payload[1] & 0xff) << 24)
                            | ((payload[2] & 0xff) << 16)
                            | ((payload[3] & 0xff) << 8)
                            | (payload[4] & 0xff);
                    int dataLength = ((payload[5] & 0xff) << 24)
                            | ((payload[6] & 0xff) << 16)
                            | ((payload[7] & 0xff) << 8)
                            | (payload[8] & 0xff);
                    if (payload.length != 1 + 8 + ivLength + dataLength) {
                        // not valid v1
                        return null;
                    }
                    byte[] iv = Arrays.copyOfRange(payload, 9, 9 + ivLength);
                    byte[] code = Arrays.copyOfRange(payload, 9+ivLength, payload.length);
                    String text;
                    try {
                        text = new String(KEY.decrypt(iv).doFinal(code), UTF_8);
                    } catch (GeneralSecurityException e) {
                        // it's v1 which cannot be historical, but not decrypting
                        return null;
                    }
                    return new Secret(text, iv);
                default:
                    return null;
            }
        } else {
            try {
                return HistoricalSecrets.decrypt(data, KEY);
            } catch (GeneralSecurityException e) {
                return null;
            } catch (UnsupportedEncodingException e) {
                throw new Error(e); // impossible
            } catch (IOException e) {
                return null;
            }
    /**
     * Reset the internal secret key for testing.
     */
    @Restricted(NoExternalUse.class)
    /*package*/ static void resetKeyForTest() {
        KEY.resetForTest();
    }

        key = HistoricalSecrets.getLegacyKey();
        Secret sec = HistoricalSecrets.tryDecrypt(cipher, in);
import hudson.Main;
import jenkins.model.Jenkins;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import javax.crypto.spec.IvParameterSpec;
    @Restricted(NoExternalUse.class) //TODO remove when in mainline
    public static final int DEFAULT_IV_LENGTH = 16;

                        secret = new SecretKeySpec(payload,0,128/8, KEY_ALGORITHM);
     * @deprecated use {@link #encrypt(byte[])}
    @Deprecated
            Cipher cipher = Secret.getCipher(KEY_ALGORITHM);
    /**
     * Returns a {@link Cipher} object for encrypting with this key using the provided initialization vector.
     * @param iv the initialization vector
     * @return the cipher
     */
    @Restricted(NoExternalUse.class) //TODO remove when in mainline
    public Cipher encrypt(byte[] iv) {
        try {
            Cipher cipher = Secret.getCipher(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, getKey(), new IvParameterSpec(iv));
            return cipher;
        } catch (GeneralSecurityException e) {
            throw new AssertionError(e);
        }
    }

    /**
     * Returns a {@link Cipher} object for decrypting with this key using the provided initialization vector.
     * @param iv the initialization vector
     * @return the cipher
     */
    @Restricted(NoExternalUse.class) //TODO remove when in mainline
    public Cipher decrypt(byte[] iv) {
        try {
            Cipher cipher = Secret.getCipher(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, getKey(), new IvParameterSpec(iv));
            return cipher;
        } catch (GeneralSecurityException e) {
            throw new AssertionError(e);
        }
    }

    /**
     * Generates a new Initialization Vector.
     * @param length the length of the salt
     * @return some random bytes
     * @see #encrypt(byte[])
     */
    @Restricted(NoExternalUse.class) //TODO remove when in mainline
    public byte[] newIv(int length) {
        return ConfidentialStore.get().randomBytes(length);
    }

    /**
     * Generates a new Initialization Vector of default length.
     * @return some random bytes
     * @see #newIv(int)
     * @see #encrypt(byte[])
     */
    @Restricted(NoExternalUse.class) //TODO remove when in mainline
    public byte[] newIv() {
        return newIv(DEFAULT_IV_LENGTH);
    }

     * @deprecated use {@link #decrypt(byte[])}
    @Deprecated
            Cipher cipher = Secret.getCipher(KEY_ALGORITHM);
    private static final String KEY_ALGORITHM = "AES";
    private static final String ALGORITHM = "AES/CBC/PKCS5Padding";

    /**
     * Reset the internal secret key for testing.
     */
    @Restricted(NoExternalUse.class)
    public void resetForTest() {
        if (Main.isUnitTest) {
            this.secret = null;
        } else {
            throw new IllegalStateException("Only for testing");
        }
    }
    def FOO_PATTERN = /<foo>\{[A-Za-z0-9+\/]+={0,2}}<\/foo>/
    def MSG_PATTERN = /<msg>\{[A-Za-z0-9+\/]+={0,2}}<\/msg>/
    def FOO_PATTERN2 = /(<foo>\{[A-Za-z0-9+\/]+={0,2}}<\/foo>){2}/
    def ABC_FOO_PATTERN = /<abc>\s<foo>\{[A-Za-z0-9+\/]+={0,2}}<\/foo>\s<\/abc>/

                {assert it ==~ FOO_PATTERN}

                {assert it ==~ FOO_PATTERN2}
                {assert it == "<foo>${n}</foo>"}
                {assert it == "<foo>thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret</foo>"}
        roundtrip "<foo>$o", {assert it == "<foo>$o"}
        roundtrip "$o</foo>", {assert it == "$o</foo>"}
        roundtrip "<abc>\n<foo>$o</foo>\n</abc>", {assert it ==~ ABC_FOO_PATTERN}
    void roundtrip(String before, Closure check) {
        check(f.text.replaceAll(System.getProperty("line.separator"), "\n").trim())
        //assert after.replaceAll(System.getProperty("line.separator"), "\n").trim()==f.text.replaceAll(System.getProperty("line.separator"), "\n").trim()
        cipher.init(Cipher.ENCRYPT_MODE, HistoricalSecrets.legacyKey);
        return new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8"))))
            assert new File(t,"$p/foo.xml").text.trim() ==~ MSG_PATTERN
        assert new File(t2,"foo.xml").text.trim() ==~ MSG_PATTERN
import javax.crypto.Cipher
import java.util.regex.Pattern;
    static final Pattern ENCRYPTED_VALUE_PATTERN = Pattern.compile("\\{?[A-Za-z0-9+/]+={0,2}}?");


        //Two consecutive encryption requests of the same object should result in the same encrypted value - SECURITY-304
        assert secret.encryptedValue == secret.encryptedValue
        //Two consecutive encryption requests of different objects with the same value should not result in the same encrypted value - SECURITY-304
        assert secret.encryptedValue != Secret.fromString(secret.plainText).encryptedValue
            assert ENCRYPTED_VALUE_PATTERN.matcher(ciphertext).matches();
        //Not "plain" text
        assert !ENCRYPTED_VALUE_PATTERN.matcher("hello world").matches();
        //Not "plain" text
        assert !ENCRYPTED_VALUE_PATTERN.matcher("helloworld!").matches();
        //legacy key
        assert ENCRYPTED_VALUE_PATTERN.matcher("abcdefghijklmnopqr0123456789").matches();
        //legacy key
        assert ENCRYPTED_VALUE_PATTERN.matcher("abcdefghijklmnopqr012345678==").matches();
        assert xml ==~ /<hudson\.util\.Secret>\{[A-Za-z0-9+\/]+={0,2}}<\/hudson\.util\.Secret>/
        def legacy = HistoricalSecrets.legacyKey
            def old = new String(Base64.encode(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes("UTF-8"))))
/*
 * The MIT License
 *
 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
 * Copyright (c) 2016, CloudBees Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package hudson.util;

import hudson.model.FreeStyleProject;
import hudson.model.ParameterDefinition;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.PasswordParameterDefinition;
import org.hamcrest.core.Is;
import org.junit.After;
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.recipes.LocalData;

import java.io.IOException;
import java.util.regex.Pattern;

import static org.hamcrest.core.Is.isA;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.*;

/**
 * Tests {@link Secret}.
 */
public class SecretCompatTest {

    @Rule
    public JenkinsRule j = new JenkinsRule() {
        @Override
        public void before() throws Throwable {
            Secret.resetKeyForTest();  //As early as possible
            super.before();
        }
    };

    @After
    public void after() {
        Secret.resetKeyForTest();
    }


    @Test
    @Issue("SECURITY-304")
    public void encryptedValueStaysTheSameAfterRoundtrip() throws Exception {
        FreeStyleProject project = j.createFreeStyleProject();
        project.addProperty(new ParametersDefinitionProperty(new PasswordParameterDefinition("p", "s3cr37", "Keep this a secret")));
        project = j.configRoundtrip(project);
        String round1 = project.getConfigFile().asString();
        project = j.configRoundtrip(project);
        String round2 = project.getConfigFile().asString();
        assertEquals(round1, round2);


        //But reconfiguring will make it a new value
        project = j.jenkins.getItemByFullName(project.getFullName(), FreeStyleProject.class);
        project.removeProperty(ParametersDefinitionProperty.class);
        project.addProperty(new ParametersDefinitionProperty(new PasswordParameterDefinition("p", "s3cr37", "Keep this a secret")));
        project = j.configRoundtrip(project);
        String round3 = project.getConfigFile().asString();
        assertNotEquals(round2, round3);
        //Saving again will produce the same
        project = j.configRoundtrip(project);
        String round4 = project.getConfigFile().asString();
        assertEquals(round3, round4);
    }

    @Test
    @Issue("SECURITY-304")
    @LocalData
    public void canReadPreSec304Secrets() throws Exception {
        FreeStyleProject project = j.jenkins.getItemByFullName("OldSecret", FreeStyleProject.class);
        String oldxml = project.getConfigFile().asString();
        //It should be unchanged on disk
        assertThat(oldxml, containsString("<defaultValue>z/Dd3qrHdQ6/C5lR7uEafM/jD3nQDrGprw3XsfZ/0vo=</defaultValue>"));
        ParametersDefinitionProperty property = project.getProperty(ParametersDefinitionProperty.class);
        ParameterDefinition definition = property.getParameterDefinitions().get(0);
        assertTrue(definition instanceof PasswordParameterDefinition);
        Secret secret = ((PasswordParameterDefinition) definition).getDefaultValueAsSecret();
        assertEquals("theSecret", secret.getPlainText());

        //OK it was read correctly from disk, now the first roundtrip should update the encrypted value

        project = j.configRoundtrip(project);
        String newXml = project.getConfigFile().asString();
        assertNotEquals(oldxml, newXml); //This could have changed because Jenkins has moved on, so not really a good check
        assertThat(newXml, not(containsString("<defaultValue>z/Dd3qrHdQ6/C5lR7uEafM/jD3nQDrGprw3XsfZ/0vo=</defaultValue>")));
        Pattern p = Pattern.compile("<defaultValue>\\{[A-Za-z0-9+/]+={0,2}}</defaultValue>");
        assertTrue(p.matcher(newXml).find());

        //But the next roundtrip should result in the same data
        project = j.configRoundtrip(project);
        String round2 = project.getConfigFile().asString();
        assertEquals(newXml, round2);
    }
}
import org.hamcrest.CoreMatchers;
import java.util.regex.Pattern;

import static org.junit.Assert.assertThat;
    final String plain_regex_match = ".*\\{[A-Za-z0-9+/]+={0,2}}.*";

        Pattern pattern = Pattern.compile("<foo>"+plain_regex_match+"</foo>");
        assertTrue(pattern.matcher(FileUtils.readFileToString(xml).trim()).matches());
import java.util.regex.Pattern;

import static org.hamcrest.core.Is.is;
    // TODO in trunk switch to @Issue({"SECURITY-266","SECURITY-304"})
    @Issue("SECURITY-266, SECURITY-304")

            //final String plain_regex_match = ".*\\{[A-Za-z0-9+/]+={0,2}}.*";
            final String xml_regex_match = "\\{[A-Za-z0-9+/]+={0,2}}";
            final Pattern xml_regex_pattern = Pattern.compile(xml_regex_match);
            final String staticTest = "\n\nvalue=\"{AQAAABAAAAAgXhXgopokysZkduhl+v1gm0UhUBBbjKDVpKz7bGk3mIO53cNTRdlu7LC4jZYEc+vF}\"\n";
            //Just a quick verification on what could be on the page and that the regexp is correctly set up
            assertThat(xml_regex_pattern.matcher(staticTest).find(), is(true));

            //String sEnc = s.getEncryptedValue();
            assertThat(xml_regex_pattern.matcher(configure.getWebResponse().getContentAsString()).find(), is(true));

            assertThat(Pattern.compile("<secret>" + xml_regex_match + "</secret>").matcher(xmlAdmin).find(), is(true));
            assertThat(xml_regex_pattern.matcher(configure.getWebResponse().getContentAsString()).find(), is(false));
            assertThat(xml_regex_pattern.matcher(xmlDev).find(), is(false));
            assertEquals(xmlAdmin.replaceAll(xml_regex_match, "********"), xmlDev);
<?xml version='1.0' encoding='UTF-8'?>
<hudson>
    <disabledAdministrativeMonitors/>
    <version>1.625.4-SNAPSHOT (private-12/16/2016 18:04 GMT-rsandell)</version>
    <numExecutors>2</numExecutors>
    <mode>NORMAL</mode>
    <useSecurity>true</useSecurity>
    <authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
    <securityRealm class="hudson.security.SecurityRealm$None"/>
    <disableRememberMe>false</disableRememberMe>
    <projectNamingStrategy class="jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy"/>
    <workspaceDir>${ITEM_ROOTDIR}/workspace</workspaceDir>
    <buildsDir>${ITEM_ROOTDIR}/builds</buildsDir>
    <jdks/>
    <viewsTabBar class="hudson.views.DefaultViewsTabBar"/>
    <myViewsTabBar class="hudson.views.DefaultMyViewsTabBar"/>
    <clouds/>
    <quietPeriod>5</quietPeriod>
    <scmCheckoutRetryCount>0</scmCheckoutRetryCount>
    <views>
        <hudson.model.AllView>
            <owner class="hudson" reference="../../.."/>
            <name>All</name>
            <filterExecutors>false</filterExecutors>
            <filterQueue>false</filterQueue>
            <properties class="hudson.model.View$PropertyList"/>
        </hudson.model.AllView>
    </views>
    <primaryView>All</primaryView>
    <slaveAgentPort>0</slaveAgentPort>
    <label></label>
    <nodeProperties/>
    <globalNodeProperties/>
</hudson>
<?xml version='1.0' encoding='UTF-8'?>
<project>
  <actions/>
  <description></description>
  <keepDependencies>false</keepDependencies>
  <properties>
    <hudson.model.ParametersDefinitionProperty>
      <parameterDefinitions>
        <hudson.model.PasswordParameterDefinition>
          <name>alice</name>
          <description>theSecret</description>
          <defaultValue>z/Dd3qrHdQ6/C5lR7uEafM/jD3nQDrGprw3XsfZ/0vo=</defaultValue>
        </hudson.model.PasswordParameterDefinition>
      </parameterDefinitions>
    </hudson.model.ParametersDefinitionProperty>
  </properties>
  <scm class="hudson.scm.NullSCM"/>
  <canRoam>true</canRoam>
  <disabled>false</disabled>
  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
  <triggers/>
  <concurrentBuild>false</concurrentBuild>
  <builders/>
  <publishers/>
  <buildWrappers/>
</project>
4311ab5e95e3da7b9b1360b52cac6f0f666db7b48f9f701296cc07c8f00612b451f1874e584d49560810619e8a6ff6b19f8f58ae1305c515fc62a7b60ea3a69e6058cad16b2c8df317952b749fdaaecab013431da55bb4ea4b8eee754fa043261b51a99a2b537fd57f867cdcb1e209f3bba735a8672dbfc3f10b0e2209a81683