提交 c9ccc671 编写于 作者: J Jesse Glick

Merge branch 'master' of github.com:jenkinsci/jenkins

......@@ -55,6 +55,13 @@ Upcoming changes</a>
<!-- Record your changes in the trunk here. -->
<div id="trunk" style="display:none"><!--=TRUNK-BEGIN=-->
<ul class=image>
<li class=>
</ul>
</div><!--=TRUNK-END=-->
<!-- these changes are controlled by the release process. DO NOT MODIFY -->
<div id="rc" style="display:none;"><!--=BEGIN=-->
<h3><a name=v1.499>What's new in 1.499</a> <!--=DATE=--></h3>
<li class=bug>
Since 1.494, when signing up as a new user in the private security realm the email address was left unconfigured and a stack trace printed.
<li class=rfe>
......@@ -66,19 +73,19 @@ Upcoming changes</a>
<li class=bug>
Slow rendering of view pages in large installations due to eager check whether the “People” link would show anything.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-16244">issue 16244</a>)
</div><!--=END=-->
<h3><a name=v1.498>What's new in 1.498</a> (2013/01/07)</h3>
<ul class=image>
<li class='major bug'>
The master key that was protecting all the sensitive data in <tt>$JENKINS_HOME</tt> was vulnerable.
(SECURITY-49)
</ul>
</div><!--=TRUNK-END=-->
<!-- these changes are controlled by the release process. DO NOT MODIFY -->
<div id="rc" style="display:none;"><!--=BEGIN=-->
<h3><a name=v1.497>What's new in 1.497</a> <!--=DATE=--></h3>
<h3><a name=v1.497>What's new in 1.497</a> (2013/01/06)</h3>
<ul class=image>
<li class=bug>
Delete the oldest build but it still come up on HistoryWidget
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-16194">issue 16194</a>)
</ul>
</div><!--=END=-->
<h3><a name=v1.496>What's new in 1.496</a> (2012/12/30)</h3>
<ul class=image>
<li class=bug>
......
......@@ -5,7 +5,7 @@
<parent>
<artifactId>pom</artifactId>
<groupId>org.jenkins-ci.main</groupId>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
</parent>
<artifactId>cli</artifactId>
......
......@@ -29,7 +29,7 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
......@@ -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>
......
......@@ -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();
......
......@@ -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())
......
......@@ -31,6 +31,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;
......@@ -118,8 +119,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)),
......@@ -133,8 +133,6 @@ public class AnnotatedLargeText<T> extends LargeText {
ois.close();
}
}
} catch (GeneralSecurityException e) {
throw new IOException2(e);
} catch (ClassNotFoundException e) {
throw new IOException2(e);
}
......@@ -160,21 +158,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");
}
......@@ -260,6 +260,12 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
}
}
@Override
public synchronized void save() throws IOException {
super.save();
updateTransientActions();
}
@Override
public void onCreatedFromScratch() {
super.onCreatedFromScratch();
......
......@@ -38,16 +38,16 @@ import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
/**
* Checks the health of a subsystem of Hudson and if there's something
* Checks the health of a subsystem of Jenkins and if there's something
* that requires administrator's attention, notify the administrator.
*
* <h2>How to implement?</h2>
* <p>
* Plugins who wish to contribute such notifications can implement this
* class and put {@link Extension} on it to register it to Hudson.
* class and put {@link Extension} on it to register it to Jenkins.
*
* <p>
* Once installed, it's the implementor's responsibility to perform
* Once installed, it's the implementer's responsibility to perform
* monitoring and activate/deactivate the monitor accordingly. Sometimes
* this can be done by updating a flag from code (see {@link SCMTrigger}
* for one such example), while other times it's more convenient to do
......@@ -63,7 +63,7 @@ import org.kohsuke.stapler.StaplerResponse;
* <dd>
* If {@link #isActivated()} returns true, Hudson will use the <tt>message.jelly</tt>
* view of this object to render the warning text. This happens in the
* <tt>http://SERVER/hudson/manage</tt> page. This view should typically render
* <tt>http://SERVER/jenkins/manage</tt> page. This view should typically render
* a DIV box with class='error' or class='warning' with a human-readable text
* inside it. It often also contains a link to a page that provides more details
* about the problem.
......
......@@ -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);
}
......@@ -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) {
......
......@@ -43,6 +43,7 @@ import org.jvnet.localizer.Localizable;
* Each permission is represented by a specific instance of {@link Permission}.
*
* @author Kohsuke Kawaguchi
* @see https://wiki.jenkins-ci.org/display/JENKINS/Making+your+plugin+behave+in+secured+Jenkins
*/
public final class Permission {
......
......@@ -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>
......
......@@ -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;
......
......@@ -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");
}
......@@ -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();
}
......
......@@ -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.
......
......@@ -31,6 +31,7 @@ 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.SecretKey;
......@@ -44,8 +45,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 +97,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 GeneralSecurityException {
String secret = SECRET;
if(secret==null) return Jenkins.getInstance().getSecretKeyAsAES128();
return Util.toAes128Key(secret);
......@@ -111,8 +116,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 +133,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 +150,17 @@ public final class Secret implements Serializable {
}
}
/*package*/ static Secret tryDecrypt(Cipher cipher, byte[] in) throws UnsupportedEncodingException {
try {
String plainText = new String(cipher.doFinal(in), "UTF-8");
if(plainText.endsWith(MAGIC))
return new Secret(plainText.substring(0,plainText.length()-MAGIC.length()));
return null;
} catch (GeneralSecurityException e) {
return null; // if the key doesn't match with the bytes, it can result in BadPaddingException
}
}
/**
* 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 +224,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 {
......
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;
}
}
......@@ -196,6 +196,9 @@ import jenkins.ExtensionComponentSet;
import jenkins.ExtensionRefreshException;
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;
......@@ -775,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 {
......@@ -995,6 +1002,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;
......@@ -1003,11 +1014,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.
*/
......@@ -1485,7 +1514,7 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro
}
/**
* Returns true if the current running Hudson is upgraded from a version earlier than the specified version.
* Returns true if the current running Jenkins is upgraded from a version earlier than the specified version.
*
* <p>
* This method continues to return true until the system configuration is saved, at which point
......
......@@ -62,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) {
......@@ -103,10 +109,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 +129,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);
}
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;
}
}
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
* 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 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
*/
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();
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());
}
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";
}
package jenkins.security;
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
*/
// @MetaInfServices --- not annotated because this is the fallback implementation
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();
}
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";
}
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);
}
}
}
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.
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();
}
/**
* 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());
}
......@@ -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");
}
......@@ -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);
......
package jenkins.util.io;
import jenkins.model.Jenkins;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Uses a presence/absence of a file as a persisted boolean storage.
*
* <p>
* This is convenient when you need to store just a few bits of infrequently accessed information
* as you can forget the explicit persistence of it. This class masks I/O problem, so if the persistence
* fails, you'll get no error report.
*
* @author Kohsuke Kawaguchi
* @since 1.498
*/
public class FileBoolean {
private final File file;
public FileBoolean(File file) {
this.file = file;
}
public FileBoolean(Class owner, String name) {
this(new File(Jenkins.getInstance().getRootDir(),owner.getName().replace('$','.')+'/'+name));
}
/**
* Gets the current state. True if the file exists, false if it doesn't.
*/
public boolean get() {
return file.exists();
}
public boolean isOn() { return get(); }
public boolean isOff() { return !get(); }
public void set(boolean b) {
if (b) on(); else off();
}
public void on() {
try {
file.getParentFile().mkdirs();
new FileOutputStream(file).close();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to touch "+file);
}
}
public void off() {
file.delete();
}
private static final Logger LOGGER = Logger.getLogger(FileBoolean.class.getName());
}
......@@ -48,6 +48,7 @@ import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
......@@ -239,6 +240,13 @@ public class XStreamDOM {
return new ConverterImpl().unmarshalElement(in, null);
}
public Map<String, String> getAttributeMap() {
Map<String,String> r = new HashMap<String, String>();
for (int i=0; i<attributes.length; i+=2)
r.put(attributes[i],attributes[i+1]);
return r;
}
private static class ReaderImpl extends AbstractXmlReader implements DocumentReader {
private static class Pointer {
final XStreamDOM node;
......
......@@ -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>
/*
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())
}
}
}
}
/*
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"))
}
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
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();
}
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();
}
}
......@@ -21,54 +21,53 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.util;
package hudson.util
import jenkins.model.Jenkins;
import junit.framework.TestCase;
import com.trilead.ssh2.crypto.Base64;
import jenkins.model.Jenkins
import jenkins.security.ConfidentialStoreRule;
import org.junit.Rule
import org.junit.Test
import java.security.SecureRandom;
import hudson.Util;
import javax.crypto.Cipher;
/**
* @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);
public class SecretTest {
@Rule
public ConfidentialStoreRule confidentialStore = new ConfidentialStoreRule()
}
@Rule
public MockSecretRule mockSecretRule = new MockSecretRule()
@Override protected void tearDown() throws Exception {
Secret.SECRET = null;
}
public void testEncrypt() {
Secret secret = Secret.fromString("abc");
assertEquals("abc",secret.getPlainText());
@Test
void testEncrypt() {
def secret = Secret.fromString("abc");
assert "abc"==secret.plainText;
// make sure we got some encryption going
System.out.println(secret.getEncryptedValue());
assertTrue(!"abc".equals(secret.getEncryptedValue()));
println secret.encryptedValue;
assert !"abc".equals(secret.encryptedValue);
// can we round trip?
assertEquals(secret,Secret.fromString(secret.getEncryptedValue()));
assert secret==Secret.fromString(secret.encryptedValue);
}
public void testDecrypt() {
assertEquals("abc",Secret.toString(Secret.fromString("abc")));
@Test
void testDecrypt() {
assert "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);
@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 {
......@@ -78,11 +77,28 @@ public class SecretTest extends TestCase {
/**
* 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();
@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);
assertEquals("secret",Secret.toString(foo.password));
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"
}
}
}
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>();
}
}
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
}
}
}
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;
}
}
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)
}
}
}
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()
}
}
jenkins (1.498) unstable; urgency=low
* See http://jenkins-ci.org/changelog for more details.
-- Kohsuke Kawaguchi <kk@kohsuke.org> Mon, 07 Jan 2013 11:12:53 -0800
jenkins (1.497) unstable; urgency=low
* See http://jenkins-ci.org/changelog for more details.
-- Kohsuke Kawaguchi <kk@kohsuke.org> Sun, 06 Jan 2013 13:59:18 -0800
jenkins (1.496) unstable; urgency=low
* See http://jenkins-ci.org/changelog for more details.
......
......@@ -29,7 +29,7 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
</parent>
<artifactId>maven-plugin</artifactId>
......
......@@ -11,7 +11,7 @@
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<name>Jenkins plugin POM</name>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
<packaging>pom</packaging>
<!--
......@@ -34,7 +34,7 @@
<dependency><!-- if a plugin wants to depend on the maven plugin, choose the right version automatically -->
<groupId>org.jenkins-ci.main</groupId>
<artifactId>maven-plugin</artifactId>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
......@@ -44,25 +44,25 @@
<groupId>org.jenkins-ci.main</groupId>
<artifactId>jenkins-war</artifactId>
<type>war</type>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>jenkins-core</artifactId>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>jenkins-test-harness</artifactId>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>ui-samples-plugin</artifactId>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
......
......@@ -33,7 +33,7 @@ THE SOFTWARE.
<groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Jenkins main module</name>
......@@ -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>
......
......@@ -29,7 +29,7 @@ THE SOFTWARE.
<parent>
<artifactId>pom</artifactId>
<groupId>org.jenkins-ci.main</groupId>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
</parent>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>jenkins-test-harness</artifactId>
......
package hudson.util;
/**
* @author Kohsuke Kawaguchi
*/
public class SecretHelper {
public static void set(String s) {
Secret.SECRET = s;
}
}
......@@ -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());
}
}
}
......@@ -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")));
}
}
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";
}
......@@ -29,7 +29,7 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
</parent>
<artifactId>ui-samples-plugin</artifactId>
......
......@@ -28,7 +28,7 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>pom</artifactId>
<version>1.498-SNAPSHOT</version>
<version>1.499-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册