CVE-2013-0158 (published 2013-02-24T22:55Z)

Unspecified vulnerability in Jenkins before 1.498, Jenkins LTS before 1.480.2, and Jenkins Enterprise 1.447.x before 1.447.6.1 and 1.466.x before 1.466.12.1, when a slave is attached and anonymous read access is enabled, allows remote attackers to obtain the master cryptographic key via unknown vectors.

Severity: LOW
CVSS v2 base score: 2.6 (AV:N/AC:H/Au:N/C:P/I:N/A:N)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/jenkinsci/jenkins
commit_details: repo_url: https://github.com/jenkinsci/jenkins
author: Kohsuke Kawaguchi
author_date: 2013-01-05 16:55:07-08:00
msg: [SECURITY-49] actively invalidate bad API tokens.

If the user still has the API token that's generated from secret.key,
don't accept that.

Hopefully this is the last fix
repository: repo_url: https://github.com/jenkinsci/jenkins
repo_name: jenkinsci/jenkins
description: Jenkins automation server
repo_language: Java
owner: jenkinsci
file_changes: filename: ApiTokenProperty.java
change_type: ModificationType.MODIFY
diff: @@ -31,6 +31,7 @@ import hudson.model.UserProperty;
 import hudson.model.UserPropertyDescriptor;
 import hudson.util.HttpResponses;
 import hudson.util.Secret;
+import jenkins.model.Jenkins;
 import net.sf.json.JSONObject;
 import org.kohsuke.stapler.AncestorInPath;
 import org.kohsuke.stapler.DataBoundConstructor;
@@ -61,12 +62,18 @@ public class ApiTokenProperty extends UserProperty {
      * We don't let the external code set the API token,
      * but for the initial value of the token we need to compute the seed by ourselves.
      */
-    private ApiTokenProperty(String seed) {
+    /*package*/ ApiTokenProperty(String seed) {
         apiToken = Secret.fromString(seed);
     }
 
     public String getApiToken() {
-        return Util.getDigestOf(apiToken.getPlainText());
+        String p = apiToken.getPlainText();
+        if (p.equals(Util.getDigestOf(Jenkins.getInstance().getSecretKey()+":"+user.getId()))) {
+            // if the current token is the initial value created by pre SECURITY-49 Jenkins, we can't use that.
+            // force using the newer value
+            apiToken = Secret.fromString(p=API_KEY_SEED.mac(user.getId()));
+        }
+        return Util.getDigestOf(p);
     }
 
     public boolean matchesPassword(String password) {

programming_language: Java
filename: ApiTokenPropertyTest.java
change_type: ModificationType.MODIFY
diff: @@ -3,7 +3,9 @@ package jenkins.security;
 import com.gargoylesoftware.htmlunit.HttpWebConnection;
 import com.gargoylesoftware.htmlunit.html.HtmlForm;
 import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import hudson.Util;
 import hudson.model.User;
+import jenkins.model.Jenkins;
 import org.apache.commons.httpclient.Credentials;
 import org.apache.commons.httpclient.HttpClient;
 import org.apache.commons.httpclient.UsernamePasswordCredentials;
@@ -60,4 +62,27 @@ public class ApiTokenPropertyTest extends HudsonTestCase {
             }
         }));
     }
+
+    public void testSecurity49Upgrade() throws Exception {
+        jenkins.setSecurityRealm(createDummySecurityRealm());
+        User u = User.get("foo");
+        String historicalInitialValue = Util.getDigestOf(Jenkins.getInstance().getSecretKey() + ":" + u.getId());
+
+        // we won't accept historically used initial value as it may be compromised
+        ApiTokenProperty t = new ApiTokenProperty(historicalInitialValue);
+        u.addProperty(t);
+        String apiToken1 = t.getApiToken();
+        assertFalse(apiToken1.equals(Util.getDigestOf(historicalInitialValue)));
+
+        // the replacement for the compromised value must be consistent and cannot be random
+        ApiTokenProperty t2 = new ApiTokenProperty(historicalInitialValue);
+        u.addProperty(t2);
+        assertEquals(apiToken1,t2.getApiToken());
+
+        // any other value is OK. those are changed values
+        t = new ApiTokenProperty(historicalInitialValue+"somethingElse");
+        u.addProperty(t);
+        assertTrue(t.getApiToken().equals(Util.getDigestOf(historicalInitialValue+"somethingElse")));
+
+    }
 }

programming_language: Java
repo_url: https://github.com/jenkinsci/jenkins
commit_details: repo_url: https://github.com/jenkinsci/jenkins
author: Kohsuke Kawaguchi
author_date: 2013-01-05 15:32:41-08:00
msg: [SECURITY-49] mark secret.key generated by post SECURITY-49 Jenkins.

If JENKINS_HOME is created by a post SECURIT-49 Jenkins (LTS, and other
variants), then there's no need to alarm the user.
repository: repo_url: https://github.com/jenkinsci/jenkins
repo_name: jenkinsci/jenkins
description: Jenkins automation server
repo_language: Java
owner: jenkinsci
file_changes: filename: Jenkins.java
change_type: ModificationType.MODIFY
diff: @@ -198,6 +198,7 @@ import jenkins.InitReactorRunner;
 import jenkins.model.ProjectNamingStrategy.DefaultProjectNamingStrategy;
 import jenkins.security.ConfidentialKey;
 import jenkins.security.ConfidentialStore;
+import jenkins.util.io.FileBoolean;
 import net.sf.json.JSONObject;
 import org.acegisecurity.AccessDeniedException;
 import org.acegisecurity.AcegiSecurityException;
@@ -777,6 +778,10 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro
                 sr.nextBytes(random);
                 secretKey = Util.toHexString(random);
                 secretFile.write(secretKey);
+
+                // this marker indicates that the secret.key is generated by the version of Jenkins post SECURITY-49.
+                // this indicates that there's no need to rewrite secrets on disk
+                new FileBoolean(new File(root,"secret.key.not-so-secret")).on();
             }
 
             try {

programming_language: Java
filename: RekeySecretAdminMonitor.java
change_type: ModificationType.MODIFY
diff: @@ -64,7 +64,9 @@ public class RekeySecretAdminMonitor extends AdministrativeMonitor implements St
         // this computation needs to be done and the value be captured,
         // since $JENKINS_HOME/config.xml can be saved later before the user has
         // actually rewritten XML files.
-        if (Jenkins.getInstance().isUpgradedFromBefore(new VersionNumber("1.496.*")))
+        Jenkins j = Jenkins.getInstance();
+        if (j.isUpgradedFromBefore(new VersionNumber("1.496.*"))
+        &&  new FileBoolean(new File(j.getRootDir(),"secret.key.not-so-secret")).isOff())
             needed.on();
     }
 

programming_language: Java
repo_url: https://github.com/jenkinsci/jenkins
commit_details: repo_url: https://github.com/jenkinsci/jenkins
author: Kohsuke Kawaguchi
author_date: 2013-01-05 14:28:57-08:00
msg: [SECURITY-49] Backing off from @Extension-based discovery.

This is because we discovered some plugins that use Secret early on in
the initialization.
repository: repo_url: https://github.com/jenkinsci/jenkins
repo_name: jenkinsci/jenkins
description: Jenkins automation server
repo_language: Java
owner: jenkinsci
file_changes: filename: ConfidentialStore.java
change_type: ModificationType.MODIFY
diff: @@ -1,12 +1,20 @@
 package jenkins.security;
 
 import hudson.Extension;
+import hudson.Lookup;
+import hudson.init.InitMilestone;
+import hudson.util.Secret;
+import hudson.util.Service;
 import jenkins.model.Jenkins;
+import org.kohsuke.MetaInfServices;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nonnull;
 import java.io.IOException;
 import java.security.SecureRandom;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 /**
  * The actual storage for the data held by {@link ConfidentialKey}s, and the holder
@@ -18,8 +26,11 @@ import java.security.SecureRandom;
  *
  * <p>
  * OEM distributions of Jenkins can provide a custom {@link ConfidentialStore} implementation
- * by writing a subclass, mark it with {@link Extension} annotation, package it as a Jenkins module,
- * and bundling it with the war file.
+ * by writing a subclass, mark it with {@link MetaInfServices} annotation, package it as a Jenkins module,
+ * and bundling it with the war file. This doesn't use {@link Extension} because some plugins
+ * have been found to use {@link Secret} before we get to {@link InitMilestone#PLUGINS_PREPARED}, and
+ * therefore {@link Extension}s aren't loaded yet. (Similarly, it's conceivable that some future
+ * core code might need this early on during the boot sequence.)
  *
  * @author Kohsuke Kawaguchi
  * @since 1.498
@@ -51,11 +62,36 @@ public abstract class ConfidentialStore {
      */
     public static @Nonnull ConfidentialStore get() {
         if (TEST!=null) return TEST.get();
-        return Jenkins.getInstance().getExtensionList(ConfidentialStore.class).get(0);
+
+        Lookup lookup = Jenkins.getInstance().lookup;
+        ConfidentialStore cs = lookup.get(ConfidentialStore.class);
+        if (cs==null) {
+            try {
+                List<ConfidentialStore> r = (List) Service.loadInstances(ConfidentialStore.class.getClassLoader(), ConfidentialStore.class);
+                if (!r.isEmpty())
+                    cs = r.get(0);
+            } catch (IOException e) {
+                LOGGER.log(Level.WARNING, "Failed to list up ConfidentialStore implementations",e);
+                // fall through
+            }
+
+            if (cs==null)
+                try {
+                    cs = new DefaultConfidentialStore();
+                } catch (Exception e) {
+                    // if it's still null, bail out
+                    throw new Error(e);
+                }
+
+            cs = lookup.setIfNull(ConfidentialStore.class,cs);
+        }
+        return cs;
     }
 
     /**
      * Testing only. Used for testing {@link ConfidentialKey} without {@link Jenkins}
      */
     /*package*/ static ThreadLocal<ConfidentialStore> TEST = null;
+
+    private static final Logger LOGGER = Logger.getLogger(ConfidentialStore.class.getName());
 }

programming_language: Java
filename: DefaultConfidentialStore.java
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,5 @@
 package jenkins.security;
 
-import hudson.Extension;
 import hudson.FilePath;
 import hudson.Util;
 import hudson.util.IOException2;
@@ -28,7 +27,7 @@ import java.security.SecureRandom;
  *
  * @author Kohsuke Kawaguchi
  */
-@Extension(ordinal=-99999) // small ordinal value to allow other higher ones to take over
+// @MetaInfServices --- not annotated because this is the fallback implementation
 public class DefaultConfidentialStore extends ConfidentialStore {
     private final SecureRandom sr = new SecureRandom();
 

programming_language: Java
repo_url: https://github.com/jenkinsci/jenkins
commit_details: repo_url: https://github.com/jenkinsci/jenkins
author: Kohsuke Kawaguchi
author_date: 2013-01-02 19:02:11-08:00
msg: [SECURITY-49] Deprecating Jenkins.getSecretKey()

We are replacing it by the ConfidentialStore class and the
ConfidentialKey class, which provides purpose-specific confidential
information that are separated from each other.

In this way, not all eggs are in one basket, and in case of a
compromise, the impact will contained.

Also replaced several insecure use of digest(secret|messsage) or
digest(message|secret) by HMAC.
repository: repo_url: https://github.com/jenkinsci/jenkins
repo_name: jenkinsci/jenkins
description: Jenkins automation server
repo_language: Java
owner: jenkinsci
file_changes: filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -768,6 +768,35 @@ THE SOFTWARE.
           </archive>
         </configuration>
       </plugin>
+      <plugin><!-- run unit test in src/test/java -->
+        <groupId>org.kohsuke.gmaven</groupId>
+        <artifactId>gmaven-plugin</artifactId>
+        <!-- version specified in grandparent pom -->
+        <executions>
+          <execution>
+            <goals>
+              <goal>testCompile</goal>
+            </goals>
+            <configuration>
+              <sources>
+                <fileset>
+                  <directory>${project.basedir}/src/test/java</directory>
+                  <includes>
+                    <include>**/*.groovy</include>
+                  </includes>
+                </fileset>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+        <dependencies>
+          <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy-all</artifactId>
+            <version>1.8.5</version>
+          </dependency>
+        </dependencies>
+      </plugin>
     </plugins>
   </build>
 

programming_language: HTML
filename: DNSMultiCast.java
change_type: ModificationType.MODIFY
diff: @@ -46,7 +46,7 @@ public class DNSMultiCast implements Closeable {
                     if (tal!=null)
                         props.put("slave-port",String.valueOf(tal.getPort()));
 
-                    props.put("server-id", Util.getDigestOf(jenkins.getSecretKey()));
+                    props.put("server-id", jenkins.getLegacyInstanceId());
 
                     URL jenkins_url = new URL(rootURL);
                     int jenkins_port = jenkins_url.getPort();

programming_language: Java
filename: UDPBroadcastThread.java
change_type: ModificationType.MODIFY
diff: @@ -86,7 +86,7 @@ public class UDPBroadcastThread extends Thread {
                 StringBuilder rsp = new StringBuilder("<hudson>");
                 tag(rsp,"version", Jenkins.VERSION);
                 tag(rsp,"url", jenkins.getRootUrl());
-                tag(rsp,"server-id", Util.getDigestOf(jenkins.getSecretKey()));
+                tag(rsp,"server-id", jenkins.getLegacyInstanceId());
                 tag(rsp,"slave-port",tal==null?null:tal.getPort());
 
                 for (UDPBroadcastFragment f : UDPBroadcastFragment.all())

programming_language: Java
filename: AnnotatedLargeText.java
change_type: ModificationType.MODIFY
diff: @@ -29,6 +29,7 @@ import hudson.remoting.ObjectInputStreamEx;
 import hudson.util.IOException2;
 import hudson.util.Secret;
 import hudson.util.TimeUnit2;
+import jenkins.security.CryptoConfidentialKey;
 import org.apache.commons.io.output.ByteArrayOutputStream;
 import org.kohsuke.stapler.Stapler;
 import org.kohsuke.stapler.StaplerRequest;
@@ -116,8 +117,7 @@ public class AnnotatedLargeText<T> extends LargeText {
         try {
             String base64 = req!=null ? req.getHeader("X-ConsoleAnnotator") : null;
             if (base64!=null) {
-                Cipher sym = Secret.getCipher("AES");
-                sym.init(Cipher.DECRYPT_MODE, Jenkins.getInstance().getSecretKeyAsAES128());
+                Cipher sym = PASSING_ANNOTATOR.decrypt();
 
                 ObjectInputStream ois = new ObjectInputStreamEx(new GZIPInputStream(
                         new CipherInputStream(new ByteArrayInputStream(Base64.decode(base64.toCharArray())),sym)),
@@ -131,8 +131,6 @@ public class AnnotatedLargeText<T> extends LargeText {
                     ois.close();
                 }
             }
-        } catch (GeneralSecurityException e) {
-            throw new IOException2(e);
         } catch (ClassNotFoundException e) {
             throw new IOException2(e);
         }
@@ -158,21 +156,20 @@ public class AnnotatedLargeText<T> extends LargeText {
                 w, createAnnotator(Stapler.getCurrentRequest()), context, charset);
         long r = super.writeLogTo(start,caw);
 
-        try {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            Cipher sym = Secret.getCipher("AES");
-            sym.init(Cipher.ENCRYPT_MODE, Jenkins.getInstance().getSecretKeyAsAES128());
-            ObjectOutputStream oos = new ObjectOutputStream(new GZIPOutputStream(new CipherOutputStream(baos,sym)));
-            oos.writeLong(System.currentTimeMillis()); // send timestamp to prevent a replay attack
-            oos.writeObject(caw.getConsoleAnnotator());
-            oos.close();
-            StaplerResponse rsp = Stapler.getCurrentResponse();
-            if (rsp!=null)
-                rsp.setHeader("X-ConsoleAnnotator", new String(Base64.encode(baos.toByteArray())));
-        } catch (GeneralSecurityException e) {
-            throw new IOException2(e);
-        }
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        Cipher sym = PASSING_ANNOTATOR.encrypt();
+        ObjectOutputStream oos = new ObjectOutputStream(new GZIPOutputStream(new CipherOutputStream(baos,sym)));
+        oos.writeLong(System.currentTimeMillis()); // send timestamp to prevent a replay attack
+        oos.writeObject(caw.getConsoleAnnotator());
+        oos.close();
+        StaplerResponse rsp = Stapler.getCurrentResponse();
+        if (rsp!=null)
+            rsp.setHeader("X-ConsoleAnnotator", new String(Base64.encode(baos.toByteArray())));
         return r;
     }
 
+    /**
+     * Used for sending the state of ConsoleAnnotator to the client, because we are deserializing this object later.
+     */
+    private static final CryptoConfidentialKey PASSING_ANNOTATOR = new CryptoConfidentialKey(AnnotatedLargeText.class,"consoleAnnotator");
 }

programming_language: Java
filename: Job.java
change_type: ModificationType.MODIFY
diff: @@ -62,6 +62,7 @@ import hudson.widgets.HistoryWidget.Adapter;
 import hudson.widgets.Widget;
 import jenkins.model.Jenkins;
 import jenkins.model.ProjectNamingStrategy;
+import jenkins.security.HexStringConfidentialKey;
 import jenkins.util.io.OnMaster;
 import net.sf.json.JSONException;
 import net.sf.json.JSONObject;
@@ -308,8 +309,8 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
      */
     public EnvVars getCharacteristicEnvVars() {
         EnvVars env = new EnvVars();
-        env.put("JENKINS_SERVER_COOKIE",Util.getDigestOf("ServerID:"+ Jenkins.getInstance().getSecretKey()));
-        env.put("HUDSON_SERVER_COOKIE",Util.getDigestOf("ServerID:"+ Jenkins.getInstance().getSecretKey())); // Legacy compatibility
+        env.put("JENKINS_SERVER_COOKIE",SERVER_COOKIE.get());
+        env.put("HUDSON_SERVER_COOKIE",SERVER_COOKIE.get()); // Legacy compatibility
         env.put("JOB_NAME",getFullName());
         return env;
     }
@@ -1313,4 +1314,6 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
     public BuildTimelineWidget getTimeline() {
         return new BuildTimelineWidget(getBuilds());
     }
+
+    private final static HexStringConfidentialKey SERVER_COOKIE = new HexStringConfidentialKey(Job.class,"serverCookie",16);
 }

programming_language: Java
filename: UsageStatistics.java
change_type: ModificationType.MODIFY
diff: @@ -121,31 +121,31 @@ public class UsageStatistics extends PageDecorator {
      * Gets the encrypted usage stat data to be sent to the Hudson server.
      */
     public String getStatData() throws IOException {
-        Jenkins h = Jenkins.getInstance();
+        Jenkins j = Jenkins.getInstance();
 
         JSONObject o = new JSONObject();
         o.put("stat",1);
-        o.put("install", Util.getDigestOf(h.getSecretKey()));
-        o.put("servletContainer",h.servletContext.getServerInfo());
+        o.put("install", j.getLegacyInstanceId());
+        o.put("servletContainer", j.servletContext.getServerInfo());
         o.put("version", Jenkins.VERSION);
 
         List<JSONObject> nodes = new ArrayList<JSONObject>();
-        for( Computer c : h.getComputers() ) {
+        for( Computer c : j.getComputers() ) {
             JSONObject  n = new JSONObject();
-            if(c.getNode()==h) {
+            if(c.getNode()==j) {
                 n.put("master",true);
                 n.put("jvm-vendor", System.getProperty("java.vm.vendor"));
                 n.put("jvm-version", System.getProperty("java.version"));
             }
             n.put("executors",c.getNumExecutors());
-            DescriptorImpl descriptor = h.getDescriptorByType(DescriptorImpl.class);
+            DescriptorImpl descriptor = j.getDescriptorByType(DescriptorImpl.class);
             n.put("os", descriptor.get(c));
             nodes.add(n);
         }
         o.put("nodes",nodes);
 
         List<JSONObject> plugins = new ArrayList<JSONObject>();
-        for( PluginWrapper pw : h.getPluginManager().getPlugins() ) {
+        for( PluginWrapper pw : j.getPluginManager().getPlugins() ) {
             if(!pw.isActive())  continue;   // treat disabled plugins as if they are uninstalled
             JSONObject p = new JSONObject();
             p.put("name",pw.getShortName());
@@ -155,7 +155,7 @@ public class UsageStatistics extends PageDecorator {
         o.put("plugins",plugins);
 
         JSONObject jobs = new JSONObject();
-        List<TopLevelItem> items = h.getItems();
+        List<TopLevelItem> items = j.getItems();
         for (TopLevelItemDescriptor d : Items.all()) {
             int cnt=0;
             for (TopLevelItem item : items) {

programming_language: Java
filename: RememberMeServicesProxy.java
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,8 @@
  */
 package hudson.security;
 
+import jenkins.model.Jenkins;
+import jenkins.security.ConfidentialStore;
 import org.acegisecurity.ui.rememberme.RememberMeServices;
 import org.acegisecurity.Authentication;
 
@@ -33,9 +35,9 @@ import javax.servlet.http.HttpServletResponse;
  * {@link RememberMeServices} proxy.
  *
  * <p>
- * In Hudson, we need {@link jenkins.model.Jenkins} instance to perform remember-me service,
- * because it relies on {@link jenkins.model.Jenkins#getSecretKey()}. However, security
- * filters can be initialized before Hudson is initialized.
+ * In Jenkins, we need {@link Jenkins} instance to perform remember-me service,
+ * because it relies on {@link ConfidentialStore}. However, security
+ * filters can be initialized before Jenkins is initialized.
  * (See #1210 for example.)
  *
  * <p>

programming_language: Java
filename: SecurityRealm.java
change_type: ModificationType.MODIFY
diff: @@ -516,10 +516,21 @@ public abstract class SecurityRealm extends AbstractDescribableImpl<SecurityReal
             this.rememberMe = rememberMe;
         }
 
+        @SuppressWarnings("deprecation")
         private static RememberMeServices createRememberMeService(UserDetailsService uds) {
             // create our default TokenBasedRememberMeServices, which depends on the availability of the secret key
             TokenBasedRememberMeServices2 rms = new TokenBasedRememberMeServices2();
             rms.setUserDetailsService(uds);
+            /*
+                TokenBasedRememberMeServices needs to be used in conjunction with RememberMeAuthenticationProvider,
+                and both needs to use the same key (this is a reflection of a poor design in AcgeiSecurity, if you ask me)
+                and various security plugins have its own groovy script that configures them.
+
+                So if we change this, it creates a painful situation for those plugins by forcing them to choose
+                to work with earlier version of Jenkins or newer version of Jenkins, and not both.
+
+                So we keep this here.
+             */
             rms.setKey(Jenkins.getInstance().getSecretKey());
             rms.setParameter("remember_me"); // this is the form field name in login.jelly
             return rms;

programming_language: Java
filename: TokenBasedRememberMeServices2.java
change_type: ModificationType.MODIFY
diff: @@ -23,10 +23,10 @@
  */
 package hudson.security;
 
+import jenkins.security.HMACConfidentialKey;
 import org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices;
 import org.acegisecurity.userdetails.UserDetails;
 import org.acegisecurity.Authentication;
-import org.apache.commons.codec.digest.DigestUtils;
 
 /**
  * {@link TokenBasedRememberMeServices} with modification so as not to rely
@@ -41,7 +41,7 @@ import org.apache.commons.codec.digest.DigestUtils;
 public class TokenBasedRememberMeServices2 extends TokenBasedRememberMeServices {
     @Override
     protected String makeTokenSignature(long tokenExpiryTime, UserDetails userDetails) {
-        String expectedTokenSignature = DigestUtils.md5Hex(userDetails.getUsername() + ":" + tokenExpiryTime + ":"
+        String expectedTokenSignature = MAC.mac(userDetails.getUsername() + ":" + tokenExpiryTime + ":"
                 + "N/A" + ":" + getKey());
         return expectedTokenSignature;
     }
@@ -50,4 +50,9 @@ public class TokenBasedRememberMeServices2 extends TokenBasedRememberMeServices
     protected String retrievePassword(Authentication successfulAuthentication) {
         return "N/A";
     }
+
+    /**
+     * Used to compute the token signature securely.
+     */
+    private static final HMACConfidentialKey MAC = new HMACConfidentialKey(TokenBasedRememberMeServices.class,"mac");
 }

programming_language: Java
filename: DefaultCrumbIssuer.java
change_type: ModificationType.MODIFY
diff: @@ -118,7 +118,8 @@ public class DefaultCrumbIssuer extends CrumbIssuer {
     public static final class DescriptorImpl extends CrumbIssuerDescriptor<DefaultCrumbIssuer> implements ModelObject {
 
         public DescriptorImpl() {
-            super(Jenkins.getInstance().getSecretKey(), System.getProperty("hudson.security.csrf.requestfield", ".crumb"));
+            // salt just needs to be unique, and it doesn't have to be a secret
+            super(Jenkins.getInstance().getLegacyInstanceId(), System.getProperty("hudson.security.csrf.requestfield", ".crumb"));
             load();
         }
 

programming_language: Java
filename: SlaveComputer.java
change_type: ModificationType.MODIFY
diff: @@ -59,6 +59,7 @@ import java.security.Security;
 
 import hudson.util.io.ReopenableFileOutputStream;
 import jenkins.model.Jenkins;
+import jenkins.slaves.JnlpSlaveAgentProtocol;
 import org.kohsuke.stapler.StaplerRequest;
 import org.kohsuke.stapler.StaplerResponse;
 import org.kohsuke.stapler.QueryParameter;
@@ -128,6 +129,10 @@ public class SlaveComputer extends Computer {
         return acceptingTasks;
     }
 
+    public String getJnlpMac() {
+        return JnlpSlaveAgentProtocol.SLAVE_SECRET.mac(getName());
+    }
+
     /**
      * Allows a {@linkplain hudson.slaves.ComputerLauncher} or a {@linkplain hudson.slaves.RetentionStrategy} to
      * suspend tasks being accepted by the slave computer.

programming_language: Java
filename: Secret.java
change_type: ModificationType.MODIFY
diff: @@ -31,8 +31,11 @@ import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
 import com.trilead.ssh2.crypto.Base64;
 import jenkins.model.Jenkins;
 import hudson.Util;
+import jenkins.security.CryptoConfidentialKey;
 import org.kohsuke.stapler.Stapler;
 
+import javax.crypto.BadPaddingException;
+import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.SecretKey;
 import javax.crypto.Cipher;
 import java.io.Serializable;
@@ -44,8 +47,8 @@ import java.security.GeneralSecurityException;
  * Glorified {@link String} that uses encryption in the persisted form, to avoid accidental exposure of a secret.
  *
  * <p>
- * Note that since the cryptography relies on {@link jenkins.model.Jenkins#getSecretKey()}, this is not meant as a protection
- * against code running in the same VM, nor against an attacker who has local file system access. 
+ * This is not meant as a protection against code running in the same VM, nor against an attacker
+ * who has local file system access on Jenkins master.
  *
  * <p>
  * {@link Secret}s can correctly read-in plain text password, so this allows the existing
@@ -96,9 +99,13 @@ public final class Secret implements Serializable {
     }
 
     /**
-     * Turns {@link jenkins.model.Jenkins#getSecretKey()} into an AES key.
+     * 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.
      */
-    private static SecretKey getKey() throws UnsupportedEncodingException, GeneralSecurityException {
+    /*package*/ static SecretKey getLegacyKey() throws UnsupportedEncodingException, GeneralSecurityException {
         String secret = SECRET;
         if(secret==null)    return Jenkins.getInstance().getSecretKeyAsAES128();
         return Util.toAes128Key(secret);
@@ -111,8 +118,7 @@ public final class Secret implements Serializable {
      */
     public String getEncryptedValue() {
         try {
-            Cipher cipher = getCipher("AES");
-            cipher.init(Cipher.ENCRYPT_MODE, getKey());
+            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 (GeneralSecurityException e) {
@@ -129,12 +135,14 @@ public final class Secret implements Serializable {
     public static Secret decrypt(String data) {
         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, getKey());
-            String plainText = new String(cipher.doFinal(Base64.decode(data.toCharArray())), "UTF-8");
-            if(plainText.endsWith(MAGIC))
-                return new Secret(plainText.substring(0,plainText.length()-MAGIC.length()));
-            return null;
+            cipher.init(Cipher.DECRYPT_MODE, getLegacyKey());
+            return tryDecrypt(cipher, in);
         } catch (GeneralSecurityException e) {
             return null;
         } catch (UnsupportedEncodingException e) {
@@ -144,6 +152,17 @@ public final class Secret implements Serializable {
         }
     }
 
+    private 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;
+        }
+    }
+
     /**
      * Workaround for JENKINS-6459 / http://java.net/jira/browse/GLASSFISH-11862
      * This method uses specific provider selected via hudson.util.Secret.provider system property
@@ -207,10 +226,15 @@ public final class Secret implements Serializable {
     private static final String PROVIDER = System.getProperty(Secret.class.getName()+".provider");
 
     /**
-     * For testing only. Override the secret key so that we can test this class without {@link jenkins.model.Jenkins}.
+     * For testing only. Override the secret key so that we can test this class without {@link Jenkins}.
      */
     /*package*/ static String SECRET = null;
 
+    /**
+     * The key that encrypts the data on disk.
+     */
+    private static final CryptoConfidentialKey KEY = new CryptoConfidentialKey(Secret.class.getName());
+
     private static final long serialVersionUID = 1L;
 
     static {

programming_language: Java
filename: Jenkins.java
change_type: ModificationType.MODIFY
diff: @@ -196,6 +196,8 @@ import jenkins.ExtensionComponentSet;
 import jenkins.ExtensionRefreshException;
 import jenkins.InitReactorRunner;
 import jenkins.model.ProjectNamingStrategy.DefaultProjectNamingStrategy;
+import jenkins.security.ConfidentialKey;
+import jenkins.security.ConfidentialStore;
 import net.sf.json.JSONObject;
 import org.acegisecurity.AccessDeniedException;
 import org.acegisecurity.AcegiSecurityException;
@@ -994,6 +996,10 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro
      * Returns a secret key that survives across container start/stop.
      * <p>
      * This value is useful for implementing some of the security features.
+     *
+     * @deprecated
+     *      Due to the past security advisory, this value should not be used any more to protect sensitive information.
+     *      See {@link ConfidentialStore} and {@link ConfidentialKey} for how to store secrets.
      */
     public String getSecretKey() {
         return secretKey;
@@ -1002,11 +1008,29 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro
     /**
      * Gets {@linkplain #getSecretKey() the secret key} as a key for AES-128.
      * @since 1.308
+     * @deprecated
+     *       See {@link #getSecretKey()}.
      */
     public SecretKey getSecretKeyAsAES128() {
         return Util.toAes128Key(secretKey);
     }
 
+    /**
+     * Returns the unique identifier of this Jenkins that has been historically used to identify
+     * this Jenkins to the outside world.
+     *
+     * <p>
+     * This form of identifier is weak in that it can be impersonated by others. See
+     * https://wiki.jenkins-ci.org/display/JENKINS/Instance+Identity for more modern form of instance ID
+     * that can be challenged and verified.
+     *
+     * @since 1.498
+     */
+    @SuppressWarnings("deprecation")
+    public String getLegacyInstanceId() {
+        return Util.getDigestOf(getSecretKey());
+    }
+
     /**
      * Gets the SCM descriptor by name. Primarily used for making them web-visible.
      */

programming_language: Java
filename: ApiTokenProperty.java
change_type: ModificationType.MODIFY
diff: @@ -31,7 +31,6 @@ import hudson.model.UserProperty;
 import hudson.model.UserPropertyDescriptor;
 import hudson.util.HttpResponses;
 import hudson.util.Secret;
-import jenkins.model.Jenkins;
 import net.sf.json.JSONObject;
 import org.kohsuke.stapler.AncestorInPath;
 import org.kohsuke.stapler.DataBoundConstructor;
@@ -103,10 +102,10 @@ public class ApiTokenProperty extends UserProperty {
          * because there's no guarantee that the property is saved.
          *
          * But we also need to make sure that an attacker won't be able to guess
-         * the initial API token value. So we take the seed by hasing the instance secret key + user ID.
+         * the initial API token value. So we take the seed by hashing the secret + user ID.
          */
         public ApiTokenProperty newInstance(User user) {
-            return new ApiTokenProperty(Util.getDigestOf(Jenkins.getInstance().getSecretKey() + ":" + user.getId()));
+            return new ApiTokenProperty(API_KEY_SEED.mac(user.getId()));
         }
 
         public HttpResponse doChangeToken(@AncestorInPath User u, StaplerResponse rsp) throws IOException {
@@ -123,4 +122,9 @@ public class ApiTokenProperty extends UserProperty {
     }
 
     private static final SecureRandom RANDOM = new SecureRandom();
+
+    /**
+     * We don't want an API key that's too long, so cut the length to 16 (which produces 32-letter MAC code in hexdump)
+     */
+    private static final HMACConfidentialKey API_KEY_SEED = new HMACConfidentialKey(ApiTokenProperty.class,"seed",16);
 }

programming_language: Java
filename: ConfidentialKey.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,57 @@
+package jenkins.security;
+
+import hudson.scm.SCM;
+import hudson.tasks.Builder;
+import hudson.util.Secret;
+import jenkins.slaves.JnlpSlaveAgentProtocol;
+
+import javax.annotation.CheckForNull;
+import java.io.IOException;
+
+/**
+ * Confidential information that gets stored as a singleton in Jenkins, mostly some random token value.
+ *
+ * <p>
+ * The actual value is persisted via {@link ConfidentialStore}, but each use case that requires
+ * a secret like this should use a separate {@link ConfidentialKey} instance so that one compromised
+ * {@link ConfidentialKey} (say through incorrect usage and failure to protect it) shouldn't compromise
+ * all the others.
+ *
+ * <p>
+ * {@link ConfidentialKey} is ultimately a sequence of bytes,
+ * but for convenience, a family of subclasses are provided to represent the secret in different formats.
+ * See {@link HexStringConfidentialKey} and {@link HMACConfidentialKey} for example. In addition to the programming
+ * ease, these use case specific subtypes make it harder for vulnerability to creep in by making it harder
+ * for the secret to leak.
+ *
+ * <p>
+ * The {@link ConfidentialKey} subtypes are expected to be used as a singleton, like {@link JnlpSlaveAgentProtocol#SLAVE_SECRET}.
+ * For code that relies on XStream for persistence (such as {@link Builder}s, {@link SCM}s, and other fragment objects
+ * around builds and jobs), {@link Secret} provides more convenient way of storing secrets.
+ *
+ * @author Kohsuke Kawaguchi
+ * @see Secret
+ * @since 1.498
+ */
+public abstract class ConfidentialKey {
+    /**
+     * Name of the key. This is used as the file name.
+     */
+    private final String id;
+
+    protected ConfidentialKey(String id) {
+        this.id = id;
+    }
+
+    protected @CheckForNull byte[] load() throws IOException {
+        return ConfidentialStore.get().load(this);
+    }
+
+    protected void store(byte[] payload) throws IOException {
+        ConfidentialStore.get().store(this,payload);
+    }
+
+    public String getId() {
+        return id;
+    }
+}

programming_language: Java
filename: ConfidentialStore.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,61 @@
+package jenkins.security;
+
+import hudson.Extension;
+import jenkins.model.Jenkins;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.security.SecureRandom;
+
+/**
+ * The actual storage for the data held by {@link ConfidentialKey}s, and the holder
+ * of the master secret.
+ *
+ * <p>
+ * This class is only relevant for the implementers of {@link ConfidentialKey}s.
+ * Most plugin code should interact with {@link ConfidentialKey}s.
+ *
+ * <p>
+ * OEM distributions of Jenkins can provide a custom {@link ConfidentialStore} implementation
+ * by writing a subclass, mark it with {@link Extension} annotation, package it as a Jenkins module,
+ * and bundling it with the war file.
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.498
+ */
+public abstract class ConfidentialStore {
+    /**
+     * Persists the payload of {@link ConfidentialKey} to a persisted storage (such as disk.)
+     * The expectation is that the persisted form is secure.
+     */
+    protected abstract void store(ConfidentialKey key, byte[] payload) throws IOException;
+
+    /**
+     * Reverse operation of {@link #store(ConfidentialKey, byte[])}
+     *
+     * @return
+     *      null the data has not been previously persisted, or if the data was tampered.
+     */
+    protected abstract @CheckForNull byte[] load(ConfidentialKey key) throws IOException;
+
+    /**
+     * Works like {@link SecureRandom#nextBytes(byte[])}.
+     *
+     * This enables implementations to consult other entropy sources, if it's available.
+     */
+    public abstract byte[] randomBytes(int size);
+
+    /**
+     * Retrieves the currently active singleton instance of {@link ConfidentialStore}.
+     */
+    public static @Nonnull ConfidentialStore get() {
+        if (TEST!=null) return TEST.get();
+        return Jenkins.getInstance().getExtensionList(ConfidentialStore.class).get(0);
+    }
+
+    /**
+     * Testing only. Used for testing {@link ConfidentialKey} without {@link Jenkins}
+     */
+    /*package*/ static ThreadLocal<ConfidentialStore> TEST = null;
+}

programming_language: Java
filename: CryptoConfidentialKey.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,76 @@
+package jenkins.security;
+
+import hudson.util.Secret;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+/**
+ * {@link ConfidentialKey} that stores a {@link SecretKey} for shared-secret cryptography (AES).
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.498
+ */
+public class CryptoConfidentialKey extends ConfidentialKey {
+    private volatile SecretKey secret;
+    public CryptoConfidentialKey(String id) {
+        super(id);
+    }
+
+    public CryptoConfidentialKey(Class owner, String shortName) {
+        this(owner.getName()+'.'+shortName);
+    }
+
+    private SecretKey getKey() {
+        try {
+            if (secret==null) {
+                synchronized (this) {
+                    if (secret==null) {
+                        byte[] payload = load();
+                        if (payload==null) {
+                            payload = ConfidentialStore.get().randomBytes(256);
+                            store(payload);
+                        }
+                        // Due to the stupid US export restriction JDK only ships 128bit version.
+                        secret = new SecretKeySpec(payload,0,128/8, ALGORITHM);
+                    }
+                }
+            }
+            return secret;
+        } catch (IOException e) {
+            throw new Error("Failed to load the key: "+getId(),e);
+        }
+    }
+
+    /**
+     * Returns a {@link Cipher} object for encrypting with this key.
+     */
+    public Cipher encrypt() {
+        try {
+            Cipher cipher = Secret.getCipher(ALGORITHM);
+            cipher.init(Cipher.ENCRYPT_MODE, getKey());
+            return cipher;
+        } catch (GeneralSecurityException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    /**
+     * Returns a {@link Cipher} object for decrypting with this key.
+     */
+    public Cipher decrypt() {
+        try {
+            Cipher cipher = Secret.getCipher(ALGORITHM);
+            cipher.init(Cipher.DECRYPT_MODE, getKey());
+            return cipher;
+        } catch (GeneralSecurityException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+
+    private static final String ALGORITHM = "AES";
+}

programming_language: Java
filename: DefaultConfidentialStore.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,145 @@
+package jenkins.security;
+
+import hudson.Extension;
+import hudson.FilePath;
+import hudson.Util;
+import hudson.util.IOException2;
+import hudson.util.IOUtils;
+import hudson.util.Secret;
+import hudson.util.TextFile;
+import jenkins.model.Jenkins;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.SecretKey;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+
+/**
+ * Default portable implementation of {@link ConfidentialStore} that uses
+ * a directory inside $JENKINS_HOME.
+ *
+ * The master key is also stored in this same directory.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+@Extension(ordinal=-99999) // small ordinal value to allow other higher ones to take over
+public class DefaultConfidentialStore extends ConfidentialStore {
+    private final SecureRandom sr = new SecureRandom();
+
+    /**
+     * Directory that stores individual keys.
+     */
+    private final File rootDir;
+
+    /**
+     * The master key.
+     *
+     * The sole purpose of the master key is to encrypt individual keys on the disk.
+     * Because leaking this master key compromises all the individual keys, we must not let
+     * this master key used for any other purpose, hence the protected access.
+     */
+    private final SecretKey masterKey;
+
+    public DefaultConfidentialStore() throws IOException, InterruptedException {
+        this(new File(Jenkins.getInstance().getRootDir(),"secrets"));
+    }
+
+    public DefaultConfidentialStore(File rootDir) throws IOException, InterruptedException {
+        this.rootDir = rootDir;
+        if (rootDir.mkdirs()) {
+            // protect this directory. but don't change the permission of the existing directory
+            // in case the administrator changed this.
+            new FilePath(rootDir).chmod(0700);
+        }
+
+        TextFile masterSecret = new TextFile(new File(rootDir,"master.key"));
+        if (!masterSecret.exists()) {
+            // we are only going to use small number of bits (since export control limits AES key length)
+            // but let's generate a long enough key anyway
+            masterSecret.write(Util.toHexString(randomBytes(128)));
+        }
+        this.masterKey = Util.toAes128Key(masterSecret.readTrim());
+    }
+
+    /**
+     * Persists the payload of {@link ConfidentialKey} to the disk.
+     */
+    @Override
+    protected void store(ConfidentialKey key, byte[] payload) throws IOException {
+        CipherOutputStream cos=null;
+        FileOutputStream fos=null;
+        try {
+            Cipher sym = Secret.getCipher("AES");
+            sym.init(Cipher.ENCRYPT_MODE, masterKey);
+            cos = new CipherOutputStream(fos=new FileOutputStream(getFileFor(key)), sym);
+            cos.write(payload);
+            cos.write(MAGIC);
+        } catch (GeneralSecurityException e) {
+            throw new IOException2("Failed to persist the key: "+key.getId(),e);
+        } finally {
+            IOUtils.closeQuietly(cos);
+            IOUtils.closeQuietly(fos);
+        }
+    }
+
+    /**
+     * Reverse operation of {@link #store(ConfidentialKey, byte[])}
+     *
+     * @return
+     *      null the data has not been previously persisted.
+     */
+    @Override
+    protected byte[] load(ConfidentialKey key) throws IOException {
+        CipherInputStream cis=null;
+        FileInputStream fis=null;
+        try {
+            File f = getFileFor(key);
+            if (!f.exists())    return null;
+
+            Cipher sym = Secret.getCipher("AES");
+            sym.init(Cipher.DECRYPT_MODE, masterKey);
+            cis = new CipherInputStream(fis=new FileInputStream(f), sym);
+            byte[] bytes = IOUtils.toByteArray(cis);
+            return verifyMagic(bytes);
+        } catch (GeneralSecurityException e) {
+            throw new IOException2("Failed to persist the key: "+key.getId(),e);
+        } finally {
+            IOUtils.closeQuietly(cis);
+            IOUtils.closeQuietly(fis);
+        }
+    }
+
+    /**
+     * Verifies that the given byte[] has the MAGIC trailer, to verify the integrity of the decryption process.
+     */
+    private byte[] verifyMagic(byte[] payload) {
+        int payloadLen = payload.length-MAGIC.length;
+        if (payloadLen<0)   return null;    // obviously broken
+
+        for (int i=0; i<MAGIC.length; i++) {
+            if (payload[payloadLen+i]!=MAGIC[i])
+                return null;    // broken
+        }
+        byte[] truncated = new byte[payloadLen];
+        System.arraycopy(payload,0,truncated,0,truncated.length);
+        return truncated;
+    }
+
+    private File getFileFor(ConfidentialKey key) {
+        return new File(rootDir, key.getId());
+    }
+
+    public byte[] randomBytes(int size) {
+        byte[] random = new byte[size];
+        sr.nextBytes(random);
+        return random;
+    }
+
+    private static final byte[] MAGIC = "::::MAGIC::::".getBytes();
+}

programming_language: Java
filename: HMACConfidentialKey.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,144 @@
+package jenkins.security;
+
+import hudson.Util;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * {@link ConfidentialKey} that's used for creating a token by hashing some information with secret
+ * (such as <tt>hash(msg|secret)</tt>).
+ *
+ * <p>
+ * This provides more secure version of it by using HMAC.
+ * See http://rdist.root.org/2009/10/29/stop-using-unsafe-keyed-hashes-use-hmac/ for background.
+ * This implementation also never leaks the secret value to outside, so it makes it impossible
+ * for the careless caller to misuse the key (thus protecting ourselves from our own stupidity!)
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.498
+ */
+public class HMACConfidentialKey extends ConfidentialKey {
+    private volatile SecretKey key;
+    private final int length;
+
+    /**
+     * @param length
+     *      Byte length of the HMAC code.
+     *      By default we use HMAC-SHA256, which produces 256bit (=32bytes) HMAC,
+     *      but if different use cases requires a shorter HMAC, specify the desired length here.
+     *      Note that when using {@link #mac(String)}, string encoding causes the length to double.
+     *      So if you want to get 16-letter HMAC, you specify 8 here.
+     */
+    public HMACConfidentialKey(String id, int length) {
+        super(id);
+        this.length = length;
+    }
+
+    /**
+     * Calls into {@link #HMACConfidentialKey(String, int)} with the longest possible HMAC length.
+     */
+    public HMACConfidentialKey(String id) {
+        this(id,Integer.MAX_VALUE);
+    }
+
+    /**
+     * Calls into {@link #HMACConfidentialKey(String, int)} by combining the class name and the shortName
+     * as the ID.
+     */
+    public HMACConfidentialKey(Class owner, String shortName, int length) {
+        this(owner.getName()+'.'+shortName,length);
+    }
+
+    public HMACConfidentialKey(Class owner, String shortName) {
+        this(owner,shortName,Integer.MAX_VALUE);
+    }
+
+
+    /**
+     * Computes the message authentication code for the specified byte sequence.
+     */
+    public byte[] mac(byte[] message) {
+        return chop(createMac().doFinal(message));
+    }
+
+    /**
+     * Convenience method for verifying the MAC code.
+     */
+    public boolean checkMac(byte[] message, byte[] mac) {
+        return Arrays.equals(mac(message),mac);
+    }
+
+    /**
+     * Computes the message authentication code and return it as a string.
+     * While redundant, often convenient.
+     */
+    public String mac(String message) {
+        try {
+            return Util.toHexString(mac(message.getBytes("UTF-8")));
+        } catch (UnsupportedEncodingException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    /**
+     * Verifies MAC constructed from {@link #mac(String)}
+     */
+    public boolean checkMac(String message, String mac) {
+        return mac(message).equals(mac);
+    }
+
+    private byte[] chop(byte[] mac) {
+        if (mac.length<=length)  return mac; // already too short
+
+        byte[] b = new byte[length];
+        System.arraycopy(mac,0,b,0,b.length);
+        return b;
+    }
+
+    /**
+     * Creates a new {@link Mac} object.
+     */
+    public Mac createMac() {
+        try {
+            Mac mac = Mac.getInstance(ALGORITHM);
+            mac.init(getKey());
+            return mac;
+        } catch (GeneralSecurityException e) {
+            // Javadoc says HmacSHA256 must be supported by every Java implementation.
+            throw new Error(ALGORITHM+" not supported?",e);
+        }
+    }
+
+    private SecretKey getKey() {
+        if (key==null) {
+            synchronized (this) {
+                if (key==null) {
+                    try {
+                        byte[] encoded = load();
+                        if (encoded==null) {
+                            KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM);
+                            SecretKey key = kg.generateKey();
+                            store(encoded=key.getEncoded());
+                        }
+                        key = new SecretKeySpec(encoded,ALGORITHM);
+                    } catch (IOException e) {
+                        throw new Error("Failed to load the key: "+getId(),e);
+                    } catch (NoSuchAlgorithmException e) {
+                        throw new Error("Failed to load the key: "+getId(),e);
+                    }
+                }
+            }
+        }
+        return key;
+    }
+
+    private static final String ALGORITHM = "HmacSHA256";
+}

programming_language: Java
filename: HexStringConfidentialKey.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,65 @@
+package jenkins.security;
+
+import hudson.Util;
+
+import java.io.IOException;
+
+/**
+ * {@link ConfidentialKey} that is the random hexadecimal string of length N.
+ *
+ * <p>
+ * This is typically used as a unique ID, as a hex dump is suitable for printing, copy-pasting,
+ * as well as use as an identifier.
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.498
+ */
+public class HexStringConfidentialKey extends ConfidentialKey {
+    private final int length;
+
+    private volatile String secret;
+
+    /**
+     * @param length
+     *      Length of the hexadecimal string.
+     */
+    public HexStringConfidentialKey(String id, int length) {
+        super(id);
+        if (length%2!=0)
+            throw new IllegalArgumentException("length must be even: "+length);
+        this.length = length;
+    }
+
+    public HexStringConfidentialKey(Class owner, String shortName, int length) {
+        this(owner.getName()+'.'+shortName,length);
+    }
+
+    /**
+     * Returns the persisted hex string value.
+     *
+     * If the value isn't persisted, a new random value is created.
+     *
+     * @throws Error
+     *      If the secret fails to load. Not throwing a checked exception is for the convenience
+     *      of the caller.
+     */
+    public String get() {
+        try {
+            if (secret==null) {
+                synchronized (this) {
+                    if (secret==null) {
+                        byte[] payload = load();
+                        if (payload==null) {
+                            payload = ConfidentialStore.get().randomBytes(length/2);
+                            store(payload);
+                        }
+                        secret = Util.toHexString(payload).substring(0,length);
+                    }
+                }
+            }
+            return secret;
+        } catch (IOException e) {
+            throw new Error("Failed to load the key: "+getId(),e);
+        }
+    }
+}

programming_language: Java
filename: JnlpSlaveAgentProtocol.java
change_type: ModificationType.MODIFY
diff: @@ -8,6 +8,7 @@ import hudson.remoting.Engine;
 import hudson.slaves.SlaveComputer;
 import jenkins.AgentProtocol;
 import jenkins.model.Jenkins;
+import jenkins.security.HMACConfidentialKey;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
@@ -31,10 +32,10 @@ import java.util.logging.Logger;
  * unauthorized remote slaves.
  *
  * <p>
- * The approach here is to have {@link Jenkins#getSecretKey() a secret key} on the master.
- * This key is sent to the slave inside the <tt>.jnlp</tt> file
+ * We do this by computing HMAC of the slave name.
+ * This code is sent to the slave inside the <tt>.jnlp</tt> file
  * (this file itself is protected by HTTP form-based authentication that
- * we use everywhere else in Hudson), and the slave sends this
+ * we use everywhere else in Jenkins), and the slave sends this
  * token back when it connects to the master.
  * Unauthorized slaves can't access the protected <tt>.jnlp</tt> file,
  * so it can't impersonate a valid slave.
@@ -84,12 +85,15 @@ public class JnlpSlaveAgentProtocol extends AgentProtocol {
         }
 
         protected void run() throws IOException, InterruptedException {
-            if(!getSecretKey().equals(in.readUTF())) {
+            final String secret = in.readUTF();
+            final String nodeName = in.readUTF();
+
+            if(!SLAVE_SECRET.mac(nodeName).equals(secret)) {
                 error(out, "Unauthorized access");
                 return;
             }
 
-            final String nodeName = in.readUTF();
+
             SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName);
             if(computer==null) {
                 error(out, "No such slave: "+nodeName);
@@ -138,10 +142,6 @@ public class JnlpSlaveAgentProtocol extends AgentProtocol {
             }
         }
 
-        protected String getSecretKey() {
-            return Jenkins.getInstance().getSecretKey();
-        }
-
         protected void error(PrintWriter out, String msg) throws IOException {
             out.println(msg);
             LOGGER.log(Level.WARNING,Thread.currentThread().getName()+" is aborted: "+msg);
@@ -150,4 +150,9 @@ public class JnlpSlaveAgentProtocol extends AgentProtocol {
     }
 
     private static final Logger LOGGER = Logger.getLogger(JnlpSlaveAgentProtocol.class.getName());
+
+    /**
+     * This secret value is used as a seed for slaves.
+     */
+    public static final HMACConfidentialKey SLAVE_SECRET = new HMACConfidentialKey(JnlpSlaveAgentProtocol.class,"secret");
 }

programming_language: Java
filename: JnlpSlaveAgentProtocol2.java
change_type: ModificationType.MODIFY
diff: @@ -57,12 +57,13 @@ public class JnlpSlaveAgentProtocol2 extends JnlpSlaveAgentProtocol {
             Properties request = new Properties();
             request.load(new ByteArrayInputStream(in.readUTF().getBytes("UTF-8")));
 
-            if(!getSecretKey().equals(request.getProperty("Secret-Key"))) {
+            final String nodeName = request.getProperty("Node-Name");
+
+            if(!SLAVE_SECRET.mac(nodeName).equals(request.getProperty("Secret-Key"))) {
                 error(out, "Unauthorized access");
                 return;
             }
 
-            final String nodeName = request.getProperty("Node-Name");
             SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(nodeName);
             if(computer==null) {
                 error(out, "No such slave: "+nodeName);

programming_language: Java
filename: slave-agent.jnlp.jelly
change_type: ModificationType.MODIFY
diff: @@ -31,7 +31,9 @@ THE SOFTWARE.
   <!--
     See http://www.dallaway.com/acad/webstart/ for obtaining the certificate.
   -->
-  <l:isAdminOrTest test="true">
+  <j:getStatic var="connect" className="hudson.model.Computer" field="CONNECT"/>
+  ${it.checkPermission(connect)}
+
     <!-- See http://java.sun.com/j2se/1.5.0/docs/guide/javaws/developersguide/syntax.html for the syntax -->
     <jnlp spec="1.0+"
         codebase="${rootURL}computer/${h.encode(it.node.nodeName)}/">
@@ -61,7 +63,7 @@ THE SOFTWARE.
       </resources>
 
       <application-desc main-class="hudson.remoting.jnlp.Main">
-        <argument>${app.secretKey}</argument>
+        <argument>${it.jnlpMac}</argument>
         <argument>${it.node.nodeName}</argument>
         <j:if test="${it.launcher.tunnel!=null}">
           <argument>-tunnel</argument>
@@ -87,5 +89,4 @@ THE SOFTWARE.
         </j:if>
       </application-desc>
     </jnlp>
-  </l:isAdminOrTest>
 </j:jelly>

programming_language: HTML
filename: SecretTest.groovy
change_type: ModificationType.ADD
diff: @@ -0,0 +1,119 @@
+/*
+ * The MIT License
+ * 
+ * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
+ * 
+ * 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 jenkins.model.Jenkins
+import jenkins.security.ConfidentialStoreRule;
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+import javax.crypto.Cipher;
+import java.security.SecureRandom;
+
+import hudson.Util;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+public class SecretTest {
+    @Rule
+    public ConfidentialStoreRule confidentialStore = new ConfidentialStoreRule()
+
+    @Test @Before
+    void setUp() {
+        def sr = new SecureRandom();
+        byte[] random = new byte[32];
+        sr.nextBytes(random);
+        Secret.SECRET = Util.toHexString(random);
+    }
+
+    @Test @After
+    void tearDown() {
+        Secret.SECRET = null;
+    }
+
+    @Test
+    void testEncrypt() {
+        def secret = Secret.fromString("abc");
+        assert "abc"==secret.plainText;
+
+        // make sure we got some encryption going
+        println secret.encryptedValue;
+        assert !"abc".equals(secret.encryptedValue);
+
+        // can we round trip?
+        assert secret==Secret.fromString(secret.encryptedValue);
+    }
+
+    @Test
+    void testDecrypt() {
+        assert "abc"==Secret.toString(Secret.fromString("abc"))
+    }
+
+    @Test
+    void testSerialization() {
+        def s = Secret.fromString("Mr.Jenkins");
+        def xml = Jenkins.XSTREAM.toXML(s);
+        assert !xml.contains(s.plainText)
+        assert xml.contains(s.encryptedValue)
+
+        def o = Jenkins.XSTREAM.fromXML(xml);
+        assert o==s : xml;
+    }
+
+    public static class Foo {
+        Secret password;
+    }
+
+    /**
+     * Makes sure the serialization form is backward compatible with String.
+     */
+    @Test
+    void testCompatibilityFromString() {
+        def tagName = Foo.class.name.replace("\$","_-");
+        def xml = "<$tagName><password>secret</password></$tagName>";
+        def foo = new Foo();
+        Jenkins.XSTREAM.fromXML(xml, foo);
+        assert "secret"==Secret.toString(foo.password)
+    }
+
+    /**
+     * Secret persisted with Jenkins.getSecretKey() should still decrypt OK.
+     */
+    @Test
+    void migrationFromLegacyKeyToConfidentialStore() {
+        def legacy = Secret.legacyKey
+        ["Hello world","","\u0000unprintable"].each { str ->
+            def cipher = Secret.getCipher("AES");
+            cipher.init(Cipher.ENCRYPT_MODE, legacy);
+            def old = new String(Base64.encode(cipher.doFinal((str + Secret.MAGIC).getBytes("UTF-8"))))
+            def s = Secret.fromString(old)
+            assert s.plainText==str : "secret by the old key should decrypt"
+            assert s.encryptedValue!=old : "but when encrypting, ConfidentialKey should be in use"
+        }
+    }
+}

programming_language: Java
filename: SecretTest.java
change_type: ModificationType.DELETE
diff: @@ -1,88 +0,0 @@
-/*
- * The MIT License
- * 
- * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
- * 
- * 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 jenkins.model.Jenkins;
-import junit.framework.TestCase;
-
-import java.security.SecureRandom;
-
-import hudson.Util;
-
-/**
- * @author Kohsuke Kawaguchi
- */
-public class SecretTest extends TestCase {
-    @Override protected void setUp() throws Exception {
-        SecureRandom sr = new SecureRandom();
-        byte[] random = new byte[32];
-        sr.nextBytes(random);
-        Secret.SECRET = Util.toHexString(random);
-
-    }
-
-    @Override protected void tearDown() throws Exception {
-        Secret.SECRET = null;
-    }
-
-    public void testEncrypt() {
-        Secret secret = Secret.fromString("abc");
-        assertEquals("abc",secret.getPlainText());
-
-        // make sure we got some encryption going
-        System.out.println(secret.getEncryptedValue());
-        assertTrue(!"abc".equals(secret.getEncryptedValue()));
-
-        // can we round trip?
-        assertEquals(secret,Secret.fromString(secret.getEncryptedValue()));
-    }
-
-    public void testDecrypt() {
-        assertEquals("abc",Secret.toString(Secret.fromString("abc")));
-    }
-
-    public void testSerialization() {
-        Secret s = Secret.fromString("Mr.Hudson");
-        String xml = Jenkins.XSTREAM.toXML(s);
-        assertTrue(xml, !xml.contains(s.getPlainText()));
-        assertTrue(xml, xml.contains(s.getEncryptedValue()));
-        Object o = Jenkins.XSTREAM.fromXML(xml);
-        assertEquals(xml, o, s);
-    }
-
-    public static class Foo {
-        Secret password;
-    }
-
-    /**
-     * Makes sure the serialization form is backward compatible with String.
-     */
-    public void testCompatibilityFromString() {
-        String tagName = Foo.class.getName().replace("$","_-");
-        String xml = "<"+tagName+"><password>secret</password></"+tagName+">";
-        Foo foo = new Foo();
-        Jenkins.XSTREAM.fromXML(xml, foo);
-        assertEquals("secret",Secret.toString(foo.password));
-    }
-}

programming_language: unknown
filename: ConfidentialStoreRule.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,37 @@
+package jenkins.security;
+
+import hudson.Util;
+import org.junit.rules.ExternalResource;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Test rule that injects a temporary {@link DefaultConfidentialStore}
+ * @author Kohsuke Kawaguchi
+ */
+public class ConfidentialStoreRule extends ExternalResource {
+    public ConfidentialStore store;
+    public File tmp;
+
+    @Override
+    protected void before() throws Throwable {
+        tmp = Util.createTempDir();
+        store = new DefaultConfidentialStore(tmp);
+        ConfidentialStore.TEST.set(store);
+    }
+
+    @Override
+    protected void after() {
+        ConfidentialStore.TEST.set(null);
+        try {
+            Util.deleteRecursive(tmp);
+        } catch (IOException e) {
+            throw new Error(e);
+        }
+    }
+
+    static {
+        ConfidentialStore.TEST = new ThreadLocal<ConfidentialStore>();
+    }
+}

programming_language: Java
filename: CryptoConfidentialKeyTest.groovy
change_type: ModificationType.ADD
diff: @@ -0,0 +1,37 @@
+package jenkins.security
+
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ *
+ *
+ * @author Kohsuke Kawaguchi
+ */
+class CryptoConfidentialKeyTest {
+    @Rule
+    public ConfidentialStoreRule store = new ConfidentialStoreRule()
+
+    def key = new CryptoConfidentialKey("test")
+
+    @Test
+    void decryptGetsPlainTextBack() {
+        ["Hello world","","\u0000"].each { str ->
+            assert key.decrypt().doFinal(key.encrypt().doFinal(str.bytes))==str.bytes
+        }
+    }
+
+    @Test
+    void multipleEncryptsAreIdempotent() {
+        def str = "Hello world".bytes
+        assert key.encrypt().doFinal(str)==key.encrypt().doFinal(str)
+    }
+
+    @Test
+    void loadingExistingKey() {
+        def key2 = new CryptoConfidentialKey("test") // this will cause the key to be loaded from the disk
+        ["Hello world","","\u0000"].each { str ->
+            assert key2.decrypt().doFinal(key.encrypt().doFinal(str.bytes))==str.bytes
+        }
+    }
+}

programming_language: Scala
filename: DefaultConfidentialStoreTest.groovy
change_type: ModificationType.ADD
diff: @@ -0,0 +1,54 @@
+package jenkins.security
+
+import hudson.FilePath
+import hudson.Functions
+import hudson.Util
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+public class DefaultConfidentialStoreTest {
+
+    def tmp;
+
+    @Before
+    void setup() {
+        tmp = Util.createTempDir()
+    }
+
+    @After
+    void tearDown() {
+        Util.deleteRecursive(tmp)
+    }
+
+    @Test
+    void roundtrip() {
+        tmp.deleteDir()   // let ConfidentialStore create a directory
+
+        def store = new DefaultConfidentialStore(tmp);
+        def key = new ConfidentialKey("test") {};
+
+        // basic roundtrip
+        def str = "Hello world!"
+        store.store(key, str.bytes)
+        assert new String(store.load(key))==str
+
+        // data storage should have some stuff
+        assert new File(tmp,"test").exists()
+        assert new File(tmp,"master.key").exists()
+
+        assert !new File(tmp,"test").text.contains("Hello") // the data shouldn't be a plain text, obviously
+
+        if (!Functions.isWindows())
+            assert (new FilePath(tmp).mode()&0777) == 0700 // should be read only
+
+        // if the master key changes, we should gracefully fail to load the store
+        new File(tmp,"master.key").delete()
+        def store2 = new DefaultConfidentialStore(tmp)
+        assert new File(tmp,"master.key").exists()  // we should have a new key now
+        assert store2.load(key)==null;
+    }
+}

programming_language: Scala
filename: HMACConfidentialKeyTest.groovy
change_type: ModificationType.ADD
diff: @@ -0,0 +1,39 @@
+package jenkins.security
+
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ *
+ *
+ * @author Kohsuke Kawaguchi
+ */
+class HMACConfidentialKeyTest {
+    @Rule
+    public ConfidentialStoreRule store = new ConfidentialStoreRule()
+
+    def key = new HMACConfidentialKey("test",16)
+
+    @Test
+    void basics() {
+        def unique = [] as TreeSet
+        ["Hello world","","\u0000"].each { str ->
+            def mac = key.mac(str)
+            unique.add(mac)
+            assert mac =~ /[0-9A-Fa-f]{32}/
+            assert key.checkMac(str,mac)
+            assert !key.checkMac("garbage",mac)
+        }
+
+        assert unique.size()==3 // make sure all 3 MAC are different
+    }
+
+    @Test
+    void loadingExistingKey() {
+        // this second key of the same ID will cause it to load the key from the disk
+        def key2 = new HMACConfidentialKey("test",16)
+        ["Hello world","","\u0000"].each { str ->
+            assert key.mac(str)==key2.mac(str)
+        }
+    }
+}

programming_language: Scala
filename: HexStringConfidentialKeyTest.groovy
change_type: ModificationType.ADD
diff: @@ -0,0 +1,43 @@
+package jenkins.security
+
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ *
+ *
+ * @author Kohsuke Kawaguchi
+ */
+class HexStringConfidentialKeyTest {
+    @Rule
+    public ConfidentialStoreRule store = new ConfidentialStoreRule()
+
+    @Test
+    void hexStringShouldProduceHexString() {
+        def key = new HexStringConfidentialKey("test",8)
+        assert key.get() =~ /[A-Fa-f0-9]{8}/
+    }
+
+    @Test
+    void multipleGetsAreIdempotent() {
+        def key = new HexStringConfidentialKey("test",8)
+        assert key.get()==key.get()
+    }
+
+    @Test
+    void specifyLengthAndMakeSureItTakesEffect() {
+        [8,16,32,256].each { n ->
+            new HexStringConfidentialKey("test"+n,n).get().length()==n
+        }
+    }
+
+    @Test
+    void loadingExistingKey() {
+        def key1 = new HexStringConfidentialKey("test",8)
+        key1.get() // this causes the ke to be generated
+
+        // this second key of the same ID will cause it to load the key from the disk
+        def key2 = new HexStringConfidentialKey("test",8)
+        assert key1.get()==key2.get()
+    }
+}

programming_language: Scala
filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -452,7 +452,7 @@ THE SOFTWARE.
           <!-- see http://maven-junit-plugin.kenai.com/ for more info -->
           <groupId>org.kohsuke</groupId>
           <artifactId>maven-junit-plugin</artifactId>
-          <version>1.10</version>
+          <version>1.11</version>
          </plugin>
         <plugin>
           <groupId>org.jvnet.localizer</groupId>

programming_language: HTML
filename: JnlpAccessWithSecuredHudsonTest.java
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,7 @@
  */
 package hudson.bugs;
 
+import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
 import com.gargoylesoftware.htmlunit.Page;
 import com.gargoylesoftware.htmlunit.html.HtmlPage;
 import com.gargoylesoftware.htmlunit.xml.XmlPage;
@@ -39,6 +40,7 @@ import org.jvnet.hudson.test.HudsonTestCase;
 import org.jvnet.hudson.test.recipes.PresetData;
 import org.jvnet.hudson.test.recipes.PresetData.DataSet;
 
+import java.net.HttpURLConnection;
 import java.net.URL;
 import java.util.Collections;
 import java.util.List;
@@ -58,7 +60,7 @@ public class JnlpAccessWithSecuredHudsonTest extends HudsonTestCase {
         return new DumbSlave(name,"",System.getProperty("java.io.tmpdir")+'/'+name,"2", Mode.NORMAL, "", new JNLPLauncher(), RetentionStrategy.INSTANCE, Collections.EMPTY_LIST);
     }
 
-    @PresetData(DataSet.NO_ANONYMOUS_READACCESS)
+    @PresetData(DataSet.ANONYMOUS_READONLY)
     @Email("http://www.nabble.com/Launching-slave-by-JNLP-with-Active-Directory-plugin-and-matrix-security-problem-td18980323.html")
     public void test() throws Exception {
         jenkins.setNodes(Collections.singletonList(createNewJnlpSlave("test")));
@@ -80,5 +82,13 @@ public class JnlpAccessWithSecuredHudsonTest extends HudsonTestCase {
             Page jarResource = jnlpAgent.getPage(url);
             assertTrue(jarResource.getWebResponse().getContentType().toLowerCase(Locale.ENGLISH).startsWith("application/"));
         }
+
+
+        try {
+            jnlp = (XmlPage) jnlpAgent.goTo("computer/test/slave-agent.jnlp", "application/x-java-jnlp-file");
+            fail("anonymous users must not be able to get secrets");
+        } catch (FailingHttpStatusCodeException x) {
+            assertEquals(HttpURLConnection.HTTP_FORBIDDEN, x.getStatusCode());
+        }
     }
 }

programming_language: Java
repo_url: https://github.com/jenkinsci/jenkins
commit_details: repo_url: https://github.com/jenkinsci/jenkins
author: Kohsuke Kawaguchi
author_date: 2013-01-03 13:43:26-08:00
msg: [SECURITY-49] added a tool to re-key secrets

As an AdministrativeMonitor, it shows up in the manage Jenkins UI, and
allows the administrator to run a re-keying operation.
repository: repo_url: https://github.com/jenkinsci/jenkins
repo_name: jenkinsci/jenkins
description: Jenkins automation server
repo_language: Java
owner: jenkinsci
file_changes: filename: Secret.java
change_type: ModificationType.MODIFY
diff: @@ -34,8 +34,6 @@ import hudson.Util;
 import jenkins.security.CryptoConfidentialKey;
 import org.kohsuke.stapler.Stapler;
 
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.SecretKey;
 import javax.crypto.Cipher;
 import java.io.Serializable;
@@ -105,7 +103,7 @@ public final class Secret implements Serializable {
      * 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.
      */
-    /*package*/ static SecretKey getLegacyKey() throws UnsupportedEncodingException, GeneralSecurityException {
+    /*package*/ static SecretKey getLegacyKey() throws GeneralSecurityException {
         String secret = SECRET;
         if(secret==null)    return Jenkins.getInstance().getSecretKeyAsAES128();
         return Util.toAes128Key(secret);
@@ -152,14 +150,14 @@ public final class Secret implements Serializable {
         }
     }
 
-    private static Secret tryDecrypt(Cipher cipher, byte[] in) throws UnsupportedEncodingException {
+    /*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;
+            return null; // if the key doesn't match with the bytes, it can result in BadPaddingException
         }
     }
 

programming_language: Java
filename: SecretRewriter.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,223 @@
+package hudson.util;
+
+import com.trilead.ssh2.crypto.Base64;
+import hudson.model.TaskListener;
+import org.apache.commons.io.FileUtils;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Rewrites XML files by looking for Secrets that are stored with the old key and replaces them
+ * by the new encrypted values.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class SecretRewriter {
+    private final Cipher cipher;
+    private final SecretKey key;
+
+    /**
+     * How many files have been scanned?
+     */
+    private int count;
+
+    /**
+     * If non-null the original file before rewrite gets in here.
+     */
+    private final File backupDirectory;
+
+    /**
+     * Canonical paths of the directories we are recursing to protect
+     * against symlink induced cycles.
+     */
+    private Set<String> callstack = new HashSet<String>();
+
+    public SecretRewriter(File backupDirectory) throws GeneralSecurityException {
+        cipher = Secret.getCipher("AES");
+        key = Secret.getLegacyKey();
+        this.backupDirectory = backupDirectory;
+    }
+
+    private String tryRewrite(String s) throws IOException, InvalidKeyException {
+        if (s.length()<24)
+            return s;   // Encrypting "" in Secret produces 24-letter characters, so this must be the minimum length
+        if (!isBase64(s))
+            return s;   // decode throws IOException if the input is not base64, and this is also a very quick way to filter
+
+        byte[] in;
+        try {
+            in = Base64.decode(s.toCharArray());
+        } catch (IOException e) {
+            return s;   // not a valid base64
+        }
+        cipher.init(Cipher.DECRYPT_MODE, key);
+        Secret sec = Secret.tryDecrypt(cipher, in);
+        if(sec!=null) // matched
+            return sec.getEncryptedValue(); // replace by the new encrypted value
+        else // not encrypted with the legacy key. leave it unmodified
+            return s;
+    }
+
+    /**
+     * @param backup
+     *      if non-null, the original file will be copied here before rewriting.
+     *      if the rewrite doesn't happen, no copying.
+     */
+    public boolean rewrite(File f, File backup) throws InvalidKeyException, IOException {
+        FileInputStream fin = new FileInputStream(f);
+        try {
+            BufferedReader r = new BufferedReader(new InputStreamReader(fin, "UTF-8"));
+            AtomicFileWriter w = new AtomicFileWriter(f, "UTF-8");
+            try {
+                PrintWriter out = new PrintWriter(new BufferedWriter(w));
+
+                boolean modified = false; // did we actually change anything?
+                try {
+                    String line;
+                    StringBuilder buf = new StringBuilder();
+
+                    while ((line=r.readLine())!=null) {
+                        int copied=0;
+                        buf.setLength(0);
+                        while (true) {
+                            int sidx = line.indexOf('>',copied);
+                            if (sidx<0) break;
+                            int eidx = line.indexOf('<',sidx);
+                            if (eidx<0) break;
+
+                            String elementText = line.substring(sidx+1,eidx);
+                            String replacement = tryRewrite(elementText);
+                            if (!replacement.equals(elementText))
+                                modified = true;
+
+                            buf.append(line.substring(copied,sidx+1));
+                            buf.append(replacement);
+                            copied = eidx;
+                        }
+                        buf.append(line.substring(copied));
+                        out.println(buf.toString());
+                    }
+                } finally {
+                    out.close();
+                }
+
+                if (modified) {
+                    if (backup!=null) {
+                        backup.getParentFile().mkdirs();
+                        FileUtils.copyFile(f,backup);
+                    }
+                    w.commit();
+                }
+                return modified;
+            } finally {
+                w.abort();
+            }
+        } finally {
+            fin.close();
+        }
+    }
+
+
+    /**
+     * Recursively scans and rewrites a directory.
+     *
+     * This method shouldn't abort just because one file fails to rewrite.
+     *
+     * @return
+     *      Number of files that were actually rewritten.
+     */
+    // synchronized to prevent accidental concurrent use. this instance is not thread safe
+    public synchronized int rewriteRecursive(File dir, TaskListener listener) throws InvalidKeyException {
+        return rewriteRecursive(dir,"",listener);
+    }
+    private int rewriteRecursive(File dir, String relative, TaskListener listener) throws InvalidKeyException {
+        String canonical;
+        try {
+            canonical = dir.getCanonicalPath();
+        } catch (IOException e) {
+            canonical = dir.getAbsolutePath(); //
+        }
+        if (!callstack.add(canonical)) {
+            listener.getLogger().println("Cycle detected: "+dir);
+            return 0;
+        }
+
+        try {
+            File[] children = dir.listFiles();
+            if (children==null)     return 0;
+
+            int rewritten=0;
+            for (File child : children) {
+                String cn = child.getName();
+                if (cn.endsWith(".xml")) {
+                    if ((count++)%100==0)
+                        listener.getLogger().println("Scanning "+child);
+                    try {
+                        File backup = null;
+                        if (backupDirectory!=null)  backup = new File(backupDirectory,relative+'/'+ cn);
+                        if (rewrite(child,backup)) {
+                            if (backup!=null)
+                                listener.getLogger().println("Copied "+child+" to "+backup+" as a backup");
+                            listener.getLogger().println("Rewritten "+child);
+                            rewritten++;
+                        }
+                    } catch (IOException e) {
+                        e.printStackTrace(listener.error("Failed to rewrite "+child));
+                    }
+                }
+                if (child.isDirectory()) {
+                    if (!isIgnoredDir(child))
+                        rewritten += rewriteRecursive(child,
+                                relative.length()==0 ? cn : relative+'/'+ cn,
+                                listener);
+                }
+            }
+            return rewritten;
+        } finally {
+            callstack.remove(canonical);
+        }
+    }
+
+    /**
+     * Decides if this directory is worth visiting or not.
+     */
+    protected boolean isIgnoredDir(File dir) {
+        // ignoring the workspace and the artifacts directories. Both of them
+        // are potentially large and they do not store any secrets.
+        String n = dir.getName();
+        return n.equals("workspace") || n.equals("artifacts")
+            || n.equals("plugins") // no mutable data here
+            || n.equals("jenkins.security.RekeySecretAdminMonitor") // we don't want to rewrite backups
+            || n.equals(".") || n.equals("..");
+    }
+
+    private static boolean isBase64(char ch) {
+        return 0<=ch && ch<128 && IS_BASE64[ch];
+    }
+
+    private static boolean isBase64(String s) {
+        for (int i=0; i<s.length(); i++)
+            if (!isBase64(s.charAt(i)))
+                return false;
+        return true;
+    }
+
+    private static final boolean[] IS_BASE64 = new boolean[128];
+    static {
+        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+        for (int i=0; i<chars.length();i++)
+            IS_BASE64[chars.charAt(i)] = true;
+    }
+}

programming_language: Java
filename: RekeySecretAdminMonitor.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,196 @@
+package jenkins.security;
+
+import hudson.Extension;
+import hudson.console.AnnotatedLargeText;
+import hudson.init.InitMilestone;
+import hudson.init.Initializer;
+import hudson.model.AdministrativeMonitor;
+import hudson.util.HttpResponses;
+import hudson.util.SecretRewriter;
+import hudson.util.StreamTaskListener;
+import hudson.util.VersionNumber;
+import jenkins.model.Jenkins;
+import jenkins.util.io.FileBoolean;
+import org.kohsuke.stapler.HttpResponse;
+import org.kohsuke.stapler.StaplerProxy;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.interceptor.RequirePOST;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.security.GeneralSecurityException;
+import java.util.Date;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Warns the administrator to run {@link SecretRewriter}
+ *
+ * @author Kohsuke Kawaguchi
+ */
+@Extension
+public class RekeySecretAdminMonitor extends AdministrativeMonitor implements StaplerProxy {
+
+    /**
+     * Whether we detected a need to run the rewrite program.
+     * Once we set it to true, we'll never turn it off.
+     *
+     * If the admin decides to dismiss this warning, we use {@link #isEnabled()} for that.
+     *
+     * In this way we can correctly differentiate all the different states.
+     */
+    private final FileBoolean needed = state("needed");
+
+    /**
+     * If the scanning process has run to the completion, we set to this true.
+     */
+    private final FileBoolean done = state("done");
+
+    /**
+     * If the rewrite process is scheduled upon the next boot.
+     */
+    private final FileBoolean scanOnBoot = state("scanOnBoot");
+
+    /**
+     * Set to non-null once the rewriting activities starts running.
+     */
+    private volatile RekeyThread rekeyThread;
+
+
+    public RekeySecretAdminMonitor() throws IOException {
+        // if JENKINS_HOME existed <1.497, we need to offer rewrite
+        // this computation needs to be done and the value be captured,
+        // since $JENKINS_HOME/config.xml can be saved later before the user has
+        // actually rewritten XML files.
+        if (Jenkins.getInstance().isUpgradedFromBefore(new VersionNumber("1.496.*")))
+            needed.on();
+    }
+
+    /**
+     * Requires ADMINISTER permission for any operation in here.
+     */
+    public Object getTarget() {
+        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
+        return this;
+    }
+
+    @Override
+    public boolean isActivated() {
+        return needed.isOn();
+    }
+
+    /**
+     * Indicates that the re-keying has run to the completion.
+     */
+    public boolean isDone() {
+        return done.isOn();
+    }
+
+    public void setNeeded() {
+        needed.on();
+    }
+
+    public boolean isScanOnBoot() {
+        return scanOnBoot.isOn();
+    }
+
+    @RequirePOST
+    public HttpResponse doScan(StaplerRequest req) throws IOException, GeneralSecurityException {
+        if(req.hasParameter("background")) {
+            synchronized (this) {
+                if (!isRewriterActive()) {
+                    rekeyThread = new RekeyThread();
+                    rekeyThread.start();
+                }
+            }
+        } else
+        if(req.hasParameter("schedule")) {
+            scanOnBoot.on();
+        } else
+        if(req.hasParameter("dismiss")) {
+            disable(true);
+        } else
+            throw HttpResponses.error(400,"Invalid request submission");
+
+        return HttpResponses.redirectViaContextPath("/manage");
+    }
+
+    /**
+     * Is there an active rewriting process going on?
+     */
+    public boolean isRewriterActive() {
+        return rekeyThread !=null && rekeyThread.isAlive();
+    }
+
+    /**
+     * Used to URL-bind {@link AnnotatedLargeText}.
+     */
+    public AnnotatedLargeText getLogText() {
+        return new AnnotatedLargeText<RekeySecretAdminMonitor>(getLogFile(), Charset.defaultCharset(),
+                !isRewriterActive(),this);
+    }
+
+    private static FileBoolean state(String name) {
+        return new FileBoolean(new File(getBaseDir(),name));
+    }
+
+    @Initializer(fatal=false,after=InitMilestone.PLUGINS_STARTED,before=InitMilestone.EXTENSIONS_AUGMENTED)
+    // as early as possible, but this needs to be late enough that the ConfidentialStore is available
+    public static void scanOnReboot() throws InterruptedException, IOException, GeneralSecurityException {
+        FileBoolean flag = new RekeySecretAdminMonitor().scanOnBoot;
+        if (flag.isOn()) {
+            flag.off();
+            RekeyThread t = new RekeyThread();
+            t.start();
+            t.join();
+            // block the boot until the rewrite process is complete
+            // don't let the failure in RekeyThread block Jenkins boot.
+        }
+    }
+
+    /**
+     * Rewrite log file.
+     */
+    public static File getLogFile() {
+        return new File(getBaseDir(),"rekey.log");
+    }
+
+    private static File getBaseDir() {
+        return new File(Jenkins.getInstance().getRootDir(),RekeySecretAdminMonitor.class.getName());
+    }
+
+    private static class RekeyThread extends Thread {
+        private final SecretRewriter rewriter;
+
+        RekeyThread() throws GeneralSecurityException {
+            super("Rekey secret thread");
+            rewriter = new SecretRewriter(new File(getBaseDir(),"backups"));
+        }
+
+        @Override
+        public void run() {
+            try {
+                LOGGER.info("Initiating a re-keying of secrets. See "+getLogFile());
+                StreamTaskListener listener = new StreamTaskListener(getLogFile());
+                try {
+                    PrintStream log = listener.getLogger();
+                    log.println("Started re-keying " + new Date());
+                    int count = rewriter.rewriteRecursive(Jenkins.getInstance().getRootDir(), listener);
+                    log.printf("Completed re-keying %d files on %s\n",count,new Date());
+                    new RekeySecretAdminMonitor().done.on();
+                    LOGGER.info("Secret re-keying completed");
+                } catch (Exception e) {
+                    LOGGER.log(Level.SEVERE, "Fatal failure in re-keying secrets",e);
+                    e.printStackTrace(listener.error("Fatal failure in rewriting secrets"));
+                }
+            } catch (IOException e) {
+                LOGGER.log(Level.SEVERE, "Catastrophic failure to rewrite secrets",e);
+            }
+        }
+    }
+
+    private static final Logger LOGGER = Logger.getLogger(RekeySecretAdminMonitor.class.getName());
+
+}

programming_language: Java
filename: log.groovy
change_type: ModificationType.ADD
diff: @@ -0,0 +1,45 @@
+/*
+The MIT License
+
+Copyright (c) 2013, 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 jenkins.security.RekeySecretAdminMonitor;
+
+def l = namespace(lib.LayoutTagLib)
+def t = namespace(lib.JenkinsTagLib)
+
+l.layout {
+    l.main_panel() {
+        h1 _("Re-keying log")
+
+        if (my.isRewriterActive()) {
+            pre(id: "out")
+            div(id: "spinner") {
+                img(src: "${imagesURL}/spinner.gif", alt: "")
+            }
+            t.progressiveText(spinner: "spinner", href: "logText/progressiveHtml", idref: "out")
+        } else {
+            pre {
+                my.logText.writeHtmlTo(0, output.asWriter())
+            }
+        }
+    }
+}

programming_language: Markdown
filename: message.groovy
change_type: ModificationType.ADD
diff: @@ -0,0 +1,59 @@
+/*
+The MIT License
+
+Copyright (c) 2013, 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 jenkins.security.RekeySecretAdminMonitor;
+
+def f = namespace(lib.FormTagLib)
+
+if (!my.isDone()) {
+    div(class:"error") {
+        raw _("pleaseRekeyAsap",app.rootDir,my.url)
+    }
+}
+
+if (my.isRewriterActive()) {
+    div(class:"info") {
+        raw _("rekeyInProgress",my.url)
+    }
+} else if (my.logFile.exists()) {
+    if (my.isDone()) {
+        div(class:"info") {
+            raw _("rekeySuccessful",my.url)
+        }
+    } else {
+        div(class:"warning") {
+            raw _("rekeyHadProblems",my.url)
+        }
+    }
+}
+
+form(method:"POST",action:"${my.url}/scan",style:"text-align:center; margin-top:0.5em;",name:"rekey") {
+    f.submit(name:"background",value:_("Re-key in background now"))
+    if (my.isScanOnBoot()) {
+        input(type:"button",class:"yui-button",disabled:"true",
+                value:_("Re-keying currently scheduled during the next startup"))
+    } else {
+        f.submit(name:"schedule",  value:_("Schedule a re-key during the next startup"))
+    }
+    f.submit(name:"dismiss",   value:_("Dismiss this message"))
+}

programming_language: Swift
filename: message.properties
change_type: ModificationType.ADD
diff: @@ -0,0 +1,17 @@
+pleaseRekeyAsap=\
+    Because of <a href="https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2013-01-04">a security vulnerability</a> discovered earlier, we need to \
+    change the encryption key used to protect secrets in your configuration files on the disk. \
+    This process scans a large portion of your <tt>$JENKINS_HOME</tt> ({0}), \
+    find encrypted data, re-key them, which will take some time. \
+    See <a href="http://jenkins-ci.org/rekey">this document</a> for more implications about different ways of doing this \
+    (or not doing this.) This operation can be safely run in background, but cautious users \
+    are recommended to take backups.
+
+rekeyInProgress=Re-keying is in progress. <a href="{0}/log">You can check the log</a>.
+
+rekeySuccessful=\
+  Secrets in your <tt>$JENKINS_HOME</tt> has been re-keyed successfully. \
+  <a href="{0}/log">Please check the log</a>, confirm the success, and then dismiss or re-run.
+
+rekeyHadProblems=\
+  Re-keying has completed, but there were problems. <a href="{0}">Please check the log</a>.
\ No newline at end of file

programming_language: Markdown
filename: MockSecretRule.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,33 @@
+package hudson.util;
+
+import hudson.Util;
+import org.junit.rules.ExternalResource;
+
+import java.security.SecureRandom;
+
+/**
+ * JUnit rule that cleans that sets a temporary {@link Secret#SECRET} value.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class MockSecretRule extends ExternalResource {
+
+    private String value;
+
+    @Override
+    protected void before() throws Throwable {
+        byte[] random = new byte[32];
+        sr.nextBytes(random);
+        value = Util.toHexString(random);
+        Secret.SECRET = value;
+    }
+
+    @Override
+    protected void after() {
+        if (!Secret.SECRET.equals(value))
+            throw new IllegalStateException("Someone tinkered with Secret.SECRET");
+        Secret.SECRET = null;
+    }
+
+    private static final SecureRandom sr = new SecureRandom();
+}

programming_language: Java
filename: SecretRewriterTest.groovy
change_type: ModificationType.ADD
diff: @@ -0,0 +1,114 @@
+package hudson.util
+
+import com.trilead.ssh2.crypto.Base64
+import hudson.FilePath
+import jenkins.security.ConfidentialStoreRule
+import org.junit.Rule
+import org.junit.Test
+
+import javax.crypto.Cipher
+
+import static hudson.Util.createTempDir
+
+/**
+ *
+ *
+ * @author Kohsuke Kawaguchi
+ */
+class SecretRewriterTest {
+    @Rule
+    public MockSecretRule mockSecretRule = new MockSecretRule()
+
+    @Rule
+    public ConfidentialStoreRule confidentialStoreRule = new ConfidentialStoreRule();
+
+    @Test
+    void singleFileRewrite() {
+        def o = encryptOld('foobar') // old
+        def n = encryptNew('foobar') // new
+        roundtrip "<foo>${o}</foo>",
+                  "<foo>${n}</foo>"
+
+        roundtrip "<foo>${o}</foo><foo>${o}</foo>",
+                  "<foo>${n}</foo><foo>${n}</foo>"
+
+        roundtrip "<foo>${n}</foo>",
+                  "<foo>${n}</foo>"
+
+        roundtrip "  <foo>thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret</foo>  ",
+                  "  <foo>thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret</foo>  "
+
+        // to be rewritten, it needs to be between a tag
+        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) {
+        def sr = new SecretRewriter(null);
+        def f = File.createTempFile("test","xml");
+        try {
+            f.text = before
+            sr.rewrite(f,null)
+            assert after.trim()==f.text.trim()
+        } finally {
+            f.delete()
+        }
+    }
+
+    String encryptOld(str) {
+        def cipher = Secret.getCipher("AES");
+        cipher.init(Cipher.ENCRYPT_MODE, Secret.legacyKey);
+        return new String(Base64.encode(cipher.doFinal((str + Secret.MAGIC).getBytes("UTF-8"))))
+    }
+
+    String encryptNew(str) {
+        return Secret.fromString(str).encryptedValue
+    }
+
+    /**
+     * Directory rewrite and recursion detection
+     */
+    @Test
+    void recursionDetection() {
+        def backup = createTempDir()
+        def sw = new SecretRewriter(backup);
+        def st = StreamTaskListener.fromStdout()
+
+        def o = encryptOld("Hello world")
+        def n = encryptNew("Hello world")
+        def payload = "<msg>$o</msg>"
+        def answer = "<msg>$n</msg>"
+
+        // set up some directories with stuff
+        def t = createTempDir()
+        def dirs = ["a", "b", "c", "c/d", "c/d/e"]
+        dirs.each { p ->
+            def d = new File(t, p)
+            d.mkdir()
+            new File(d,"foo.xml").text = payload
+        }
+
+        // stuff outside
+        def t2 = createTempDir()
+        new File(t2,"foo.xml").text = payload
+
+        // some recursions as well as valid symlinks
+        new FilePath(t).child("c/symlink").symlinkTo("..",st)
+        new FilePath(t).child("b/symlink").symlinkTo(".",st)
+        new FilePath(t).child("a/symlink").symlinkTo(t2.absolutePath,st)
+
+        assert 6==sw.rewriteRecursive(t, st)
+
+        dirs.each { p->
+            assert new File(t,"$p/foo.xml").text.trim()==answer
+            assert new File(backup,"$p/foo.xml").text.trim()==payload
+        }
+
+        // t2 is only reachable by following a symlink. this should be covered, too
+        assert new File(t2,"foo.xml").text.trim()==answer.trim();
+    }
+
+}

programming_language: Scala
filename: SecretTest.groovy
change_type: ModificationType.MODIFY
diff: @@ -26,15 +26,10 @@ package hudson.util
 import com.trilead.ssh2.crypto.Base64;
 import jenkins.model.Jenkins
 import jenkins.security.ConfidentialStoreRule;
-import org.junit.After
-import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 
 import javax.crypto.Cipher;
-import java.security.SecureRandom;
-
-import hudson.Util;
 
 /**
  * @author Kohsuke Kawaguchi
@@ -43,18 +38,8 @@ public class SecretTest {
     @Rule
     public ConfidentialStoreRule confidentialStore = new ConfidentialStoreRule()
 
-    @Test @Before
-    void setUp() {
-        def sr = new SecureRandom();
-        byte[] random = new byte[32];
-        sr.nextBytes(random);
-        Secret.SECRET = Util.toHexString(random);
-    }
-
-    @Test @After
-    void tearDown() {
-        Secret.SECRET = null;
-    }
+    @Rule
+    public MockSecretRule mockSecretRule = new MockSecretRule()
 
     @Test
     void testEncrypt() {

programming_language: Scala
filename: SecretHelper.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,10 @@
+package hudson.util;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+public class SecretHelper {
+    public static void set(String s) {
+        Secret.SECRET = s;
+    }
+}

programming_language: Java
filename: RekeySecretAdminMonitorTest.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,157 @@
+package jenkins.security;
+
+import com.gargoylesoftware.htmlunit.ElementNotFoundException;
+import com.gargoylesoftware.htmlunit.html.HtmlButton;
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.trilead.ssh2.crypto.Base64;
+import hudson.FilePath;
+import hudson.Util;
+import hudson.util.Secret;
+import hudson.util.SecretHelper;
+import org.apache.commons.io.FileUtils;
+import org.jvnet.hudson.test.HudsonTestCase;
+import org.jvnet.hudson.test.recipes.Recipe.Runner;
+import org.xml.sax.SAXException;
+
+import javax.crypto.Cipher;
+import javax.inject.Inject;
+import java.io.File;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+public class RekeySecretAdminMonitorTest extends HudsonTestCase {
+    @Inject
+    RekeySecretAdminMonitor monitor;
+
+    @Override
+    protected void setUp() throws Exception {
+        SecretHelper.set(TEST_KEY);
+        super.setUp();
+        monitor.setNeeded();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        SecretHelper.set(null);
+        super.tearDown();
+    }
+
+    @Override
+    protected void recipe() throws Exception {
+        super.recipe();
+        recipes.add(new Runner() {
+            @Override
+            public void setup(HudsonTestCase testCase, Annotation recipe) throws Exception {
+            }
+
+            @Override
+            public void decorateHome(HudsonTestCase testCase, File home) throws Exception {
+                if (getName().endsWith("testScanOnBoot")) {
+                    // schedule a scan on boot
+                    File f = new File(home, RekeySecretAdminMonitor.class.getName() + "/scanOnBoot");
+                    f.getParentFile().mkdirs();
+                    new FilePath(f).touch(0);
+
+                    // and stage some data
+                    putSomeOldData(home);
+                }
+            }
+
+            @Override
+            public void tearDown(HudsonTestCase testCase, Annotation recipe) throws Exception {
+            }
+        });
+    }
+
+
+    private void putSomeOldData(File dir) throws Exception {
+        File xml = new File(dir, "foo.xml");
+        FileUtils.writeStringToFile(xml,"<foo>" + encryptOld(TEST_KEY) + "</foo>");
+    }
+
+    private void verifyRewrite(File dir) throws Exception {
+        File xml = new File(dir, "foo.xml");
+        assertEquals("<foo>" + encryptNew(TEST_KEY) + "</foo>".trim(),
+                FileUtils.readFileToString(xml).trim());
+    }
+
+    public void testBasicWorkflow() throws Exception {
+        putSomeOldData(jenkins.getRootDir());
+
+        WebClient wc = createWebClient();
+
+        // one should see the warning. try scheduling it
+        assertTrue(!monitor.isScanOnBoot());
+        HtmlForm form = getRekeyForm(wc);
+        submit(form, "schedule");
+        assertTrue(monitor.isScanOnBoot());
+        form = getRekeyForm(wc);
+        assertTrue(getButton(form, 1).isDisabled());
+
+        // run it now
+        assertTrue(!monitor.getLogFile().exists());
+        submit(form, "background");
+        assertTrue(monitor.getLogFile().exists());
+
+        // should be no warning/error now
+        HtmlPage manage = wc.goTo("/manage");
+        assertEquals(0,manage.selectNodes("//*[class='error']").size());
+        assertEquals(0,manage.selectNodes("//*[class='warning']").size());
+
+        // and the data should be rewritten
+        verifyRewrite(jenkins.getRootDir());
+        assertTrue(monitor.isDone());
+
+        // dismiss and the message will be gone
+        assertTrue(monitor.isEnabled());
+        form = getRekeyForm(wc);
+        submit(form, "dismiss");
+        assertFalse(monitor.isEnabled());
+        try {
+            getRekeyForm(wc);
+            fail();
+        } catch (ElementNotFoundException e) {
+            // expected
+        }
+    }
+
+    private HtmlForm getRekeyForm(WebClient wc) throws IOException, SAXException {
+        return wc.goTo("/manage").getFormByName("rekey");
+    }
+
+    private HtmlButton getButton(HtmlForm form, int index) {
+        return form.<HtmlButton>getHtmlElementsByTagName("button").get(index);
+    }
+
+    public void testScanOnBoot() throws Exception {
+        WebClient wc = createWebClient();
+
+        // scan on boot should have run the scan
+        assertTrue(monitor.getLogFile().exists());
+        assertTrue("scan on boot should have turned this off",!monitor.isScanOnBoot());
+
+        // and data should be migrated
+        verifyRewrite(jenkins.getRootDir());
+
+        // should be no warning/error now
+        HtmlPage manage = wc.goTo("/manage");
+        assertEquals(0,manage.selectNodes("//*[class='error']").size());
+        assertEquals(0,manage.selectNodes("//*[class='warning']").size());
+    }
+
+    private String encryptOld(String str) throws Exception {
+        Cipher cipher = Secret.getCipher("AES");
+        cipher.init(Cipher.ENCRYPT_MODE, Util.toAes128Key(TEST_KEY));
+        return new String(Base64.encode(cipher.doFinal((str + "::::MAGIC::::").getBytes("UTF-8"))));
+    }
+
+    private String encryptNew(String str) {
+        return Secret.fromString(str).getEncryptedValue();
+    }
+
+    private static final String TEST_KEY = "superDuperSecretWasNotSoSecretAfterAll";
+}

programming_language: Java