Skip to content

Commit

Permalink
Merge branch 'master' into url-dep-test-1
Browse files Browse the repository at this point in the history
  • Loading branch information
abhishekmaity authored Oct 6, 2023
2 parents 2faa504 + 778b8b6 commit f37e1b7
Show file tree
Hide file tree
Showing 22 changed files with 1,182 additions and 238 deletions.
2 changes: 1 addition & 1 deletion .gitpod/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM gitpod/workspace-full

ARG MAVEN_VERSION=3.9.4
ARG MAVEN_VERSION=3.9.5

RUN brew install gh && \
bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && sdk install maven ${MAVEN_VERSION} && sdk default maven ${MAVEN_VERSION}"
10 changes: 8 additions & 2 deletions core/src/main/java/hudson/cli/GroovyCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import hudson.Extension;
import hudson.model.User;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import jenkins.model.Jenkins;
import jenkins.util.ScriptListener;
import org.apache.commons.io.IOUtils;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
Expand Down Expand Up @@ -63,14 +65,18 @@ protected int run() throws Exception {
// this allows the caller to manipulate the JVM state, so require the execute script privilege.
Jenkins.get().checkPermission(Jenkins.ADMINISTER);

final String scriptListenerCorrelationId = String.valueOf(System.identityHashCode(this));

Binding binding = new Binding();
binding.setProperty("out", new PrintWriter(new OutputStreamWriter(stdout, getClientCharset()), true));
binding.setProperty("out", new ScriptListener.ListenerWriter(new PrintWriter(new OutputStreamWriter(stdout, getClientCharset()), true), GroovyCommand.class, null, scriptListenerCorrelationId, User.current()));
binding.setProperty("stdin", stdin);
binding.setProperty("stdout", stdout);
binding.setProperty("stderr", stderr);

GroovyShell groovy = new GroovyShell(Jenkins.get().getPluginManager().uberClassLoader, binding);
groovy.run(loadScript(), "RemoteClass", remaining.toArray(new String[0]));
String script = loadScript();
ScriptListener.fireScriptExecution(script, binding, GroovyCommand.class, null, scriptListenerCorrelationId, User.current());
groovy.run(script, "RemoteClass", remaining.toArray(new String[0]));
return 0;
}

Expand Down
30 changes: 27 additions & 3 deletions core/src/main/java/hudson/cli/GroovyshCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import groovy.lang.Binding;
import groovy.lang.Closure;
import hudson.Extension;
import hudson.model.User;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -39,6 +40,7 @@
import java.util.ArrayList;
import java.util.List;
import jenkins.model.Jenkins;
import jenkins.util.ScriptListener;
import jline.TerminalFactory;
import jline.UnsupportedTerminal;
import org.codehaus.groovy.tools.shell.Groovysh;
Expand All @@ -54,6 +56,9 @@
*/
@Extension
public class GroovyshCommand extends CLICommand {

private final String scriptListenerCorrelationId = String.valueOf(System.identityHashCode(this));

@Override
public String getShortDescription() {
return Messages.GroovyshCommand_ShortDescription();
Expand All @@ -78,6 +83,8 @@ protected int run() {
commandLine.append(arg);
}

// TODO Add binding
ScriptListener.fireScriptExecution(null, null, GroovyshCommand.class, null, scriptListenerCorrelationId, User.current());
Groovysh shell = createShell(stdin, stdout, stderr);
return shell.run(commandLine.toString());
}
Expand All @@ -96,11 +103,14 @@ protected Groovysh createShell(InputStream stdin, PrintStream stdout,
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
binding.setProperty("out", new PrintWriter(new OutputStreamWriter(stdout, charset), true));

binding.setProperty("out", new PrintWriter(new OutputStreamWriter(new ScriptListener.ListenerOutputStream(stdout, charset, GroovyshCommand.class, null, scriptListenerCorrelationId, User.current()), charset), true));
binding.setProperty("hudson", Jenkins.get()); // backward compatibility
binding.setProperty("jenkins", Jenkins.get());

IO io = new IO(new BufferedInputStream(stdin), stdout, stderr);
IO io = new IO(new BufferedInputStream(stdin),
new ScriptListener.ListenerOutputStream(stdout, charset, GroovyshCommand.class, null, scriptListenerCorrelationId, User.current()),
new ScriptListener.ListenerOutputStream(stderr, charset, GroovyshCommand.class, null, scriptListenerCorrelationId, User.current()));

final ClassLoader cl = Jenkins.get().pluginManager.uberClassLoader;
Closure registrar = new Closure(null, null) {
Expand All @@ -119,9 +129,23 @@ public Object doCall(Object[] args) {
return null;
}
};
Groovysh shell = new Groovysh(cl, binding, io, registrar);
Groovysh shell = new LoggingGroovySh(cl, binding, io, registrar);
shell.getImports().add("hudson.model.*");
return shell;
}

private class LoggingGroovySh extends Groovysh {
private final Binding binding;

LoggingGroovySh(ClassLoader cl, Binding binding, IO io, Closure registrar) {
super(cl, binding, io, registrar);
this.binding = binding;
}

@Override
protected void maybeRecordInput(String line) {
ScriptListener.fireScriptExecution(line, binding, GroovyshCommand.class, null, scriptListenerCorrelationId, User.current());
super.maybeRecordInput(line);
}
}
}
13 changes: 10 additions & 3 deletions core/src/main/java/hudson/model/UsageStatistics.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import jenkins.model.Jenkins;
import jenkins.security.FIPS140;
import jenkins.util.SystemProperties;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
Expand Down Expand Up @@ -102,8 +103,10 @@ public UsageStatistics(String keyImage) {
* Returns true if it's time for us to check for new version.
*/
public boolean isDue() {
// user opted out. no data collection.
if (!Jenkins.get().isUsageStatisticsCollected() || DISABLED) return false;
// user opted out (explicitly or FIPS is requested). no data collection
if (!Jenkins.get().isUsageStatisticsCollected() || DISABLED || FIPS140.useCompliantAlgorithms()) {
return false;
}

long now = System.currentTimeMillis();
if (now - lastAttempt > DAY) {
Expand Down Expand Up @@ -212,7 +215,11 @@ public Permission getRequiredGlobalConfigPagePermission() {
public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
try {
// for backward compatibility reasons, this configuration is stored in Jenkins
Jenkins.get().setNoUsageStatistics(json.has("usageStatisticsCollected") ? null : true);
if (DISABLED) {
Jenkins.get().setNoUsageStatistics(Boolean.TRUE);
} else {
Jenkins.get().setNoUsageStatistics(json.has("usageStatisticsCollected") ? null : Boolean.TRUE);
}
return true;
} catch (IOException e) {
throw new FormException(e, "usageStatisticsCollected");
Expand Down
155 changes: 139 additions & 16 deletions core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,22 @@
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
Expand All @@ -70,6 +76,7 @@
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import jenkins.model.Jenkins;
import jenkins.security.FIPS140;
import jenkins.security.SecurityListener;
import jenkins.security.seed.UserSeedProperty;
import jenkins.util.SystemProperties;
Expand Down Expand Up @@ -524,14 +531,23 @@ public User createAccount(String userName, String password) throws IOException {
}

/**
* Creates a new user account by registering a JBCrypt Hashed password with the user.
* Creates a new user account by registering a Hashed password with the user.
*
* @param userName The user's name
* @param hashedPassword A hashed password, must begin with {@code #jbcrypt:}
* @param hashedPassword A hashed password, must begin with {@code getPasswordHeader()}
* @see #getPasswordHeader()
*/
public User createAccountWithHashedPassword(String userName, String hashedPassword) throws IOException {
if (!PASSWORD_ENCODER.isPasswordHashed(hashedPassword)) {
throw new IllegalArgumentException("this method should only be called with a pre-hashed password");
final String message;
if (hashedPassword == null) {
message = "The hashed password cannot be null";
} else if (hashedPassword.startsWith(getPasswordHeader())) {
message = "The hashed password was hashed with the correct algorithm, but the format was not correct";
} else {
message = "The hashed password was hashed with an incorrect algorithm. Jenkins is expecting " + getPasswordHeader();
}
throw new IllegalArgumentException(message);
}
User user = User.getById(userName, true);
user.addProperty(Details.fromHashedPassword(hashedPassword));
Expand Down Expand Up @@ -885,9 +901,9 @@ public Category getCategory() {

// TODO can we instead use BCryptPasswordEncoder from Spring Security, which has its own copy of BCrypt so we could drop the special library?
/**
* {@link PasswordEncoder} that uses jBCrypt.
* {@link PasswordHashEncoder} that uses jBCrypt.
*/
private static class JBCryptEncoder implements PasswordEncoder {
static class JBCryptEncoder implements PasswordHashEncoder {
// in jBCrypt the maximum is 30, which takes ~22h with laptop late-2017
// and for 18, it's "only" 20s
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Accessible via System Groovy Scripts")
Expand All @@ -911,6 +927,7 @@ public boolean matches(CharSequence rawPassword, String encodedPassword) {
* implementation defined in jBCrypt and <a href="https://en.wikipedia.org/wiki/Bcrypt">the Wikipedia page</a>.
*
*/
@Override
public boolean isHashValid(String hash) {
Matcher matcher = BCRYPT_PATTERN.matcher(hash);
if (matcher.matches()) {
Expand All @@ -925,34 +942,131 @@ public boolean isHashValid(String hash) {
}
}

/* package */ static final JBCryptEncoder JBCRYPT_ENCODER = new JBCryptEncoder();
static class PBKDF2PasswordEncoder implements PasswordHashEncoder {

private static final String STRING_SEPARATION = ":";
private static final int KEY_LENGTH_BITS = 512;
private static final int SALT_LENGTH_BYTES = 16;
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
// ~230ms on an Intel i7-10875H CPU (JBCryptEncoder is ~57ms)
private static final int ITTERATIONS = 210_000;
private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA512";

private volatile SecureRandom random; // defer construction until we need to use it to not delay startup in the case of lack of entropy.

// $PBDKF2 is already checked before we get here.
// $algorithm(HMACSHA512) : rounds : salt_in_hex $ mac_in_hex
private static final Pattern PBKDF2_PATTERN =
Pattern.compile("^\\$HMACSHA512\\:" + ITTERATIONS + "\\:[a-f0-9]{" + (SALT_LENGTH_BYTES * 2) + "}\\$[a-f0-9]{" + ((KEY_LENGTH_BITS / 8) * 2) + "}$");

@Override
public String encode(CharSequence rawPassword) {
try {
return generatePasswordHashWithPBKDF2(rawPassword);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException("Unable to generate password with PBKDF2WithHmacSHA512", e);
}
}

@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
try {
return validatePassword(rawPassword.toString(), encodedPassword);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException("Unable to check password with PBKDF2WithHmacSHA512", e);
}
}

private String generatePasswordHashWithPBKDF2(CharSequence password) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] salt = generateSalt();
PBEKeySpec spec = new PBEKeySpec(password.toString().toCharArray(), salt, ITTERATIONS, KEY_LENGTH_BITS);
byte[] hash = generateSecretKey(spec);
return "$HMACSHA512:" + ITTERATIONS + STRING_SEPARATION + Util.toHexString(salt) + "$" + Util.toHexString(hash);
}

private static byte[] generateSecretKey(PBEKeySpec spec) throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
return secretKeyFactory.generateSecret(spec).getEncoded();
}

private SecureRandom secureRandom() {
// lazy initialisation so that we do not block startup due to entropy
if (random == null) {
synchronized (this) {
if (random == null) {
random = new SecureRandom();
}
}
}
return random;
}

private byte[] generateSalt() {
byte[] salt = new byte[SALT_LENGTH_BYTES];
secureRandom().nextBytes(salt);
return salt;
}

@Override
public boolean isHashValid(String hash) {
Matcher matcher = PBKDF2_PATTERN.matcher(hash);
return matcher.matches();
}

private static boolean validatePassword(String password, String storedPassword) throws NoSuchAlgorithmException, InvalidKeySpecException {
String[] parts = storedPassword.split("[:$]");
int iterations = Integer.parseInt(parts[2]);

byte[] salt = Util.fromHexString(parts[3]);
byte[] hash = Util.fromHexString(parts[4]);

PBEKeySpec spec = new PBEKeySpec(password.toCharArray(),
salt, iterations, hash.length * 8 /* bits in a byte */);

byte[] generatedHashValue = generateSecretKey(spec);

return MessageDigest.isEqual(hash, generatedHashValue);
}

}

/* package */ static final PasswordHashEncoder PASSWORD_HASH_ENCODER = FIPS140.useCompliantAlgorithms() ? new PBKDF2PasswordEncoder() : new JBCryptEncoder();


private static final String PBKDF2 = "$PBKDF2";
private static final String JBCRYPT = "#jbcrypt:";

/**
* Magic header used to detect if a password is hashed.
*/
private static String getPasswordHeader() {
return FIPS140.useCompliantAlgorithms() ? PBKDF2 : JBCRYPT;
}


// TODO check if DelegatingPasswordEncoder can be used
/**
* Wraps {@link #JBCRYPT_ENCODER}.
* Wraps {@link #PASSWORD_HASH_ENCODER}.
* There used to be a SHA-256-based encoder but this is long deprecated, and insecure anyway.
*/
/* package */ static class MultiPasswordEncoder implements PasswordEncoder {
/**
* Magic header used to detect if a password is bcrypt hashed.
*/
private static final String JBCRYPT_HEADER = "#jbcrypt:";

/*
CLASSIC encoder outputs "salt:hash" where salt is [a-z]+, so we use unique prefix '#jbcyrpt"
to designate JBCRYPT-format hash.
to designate JBCRYPT-format hash and $PBKDF2 to designate PBKDF2 format hash.
'#' is neither in base64 nor hex, which makes it a good choice.
*/

@Override
public String encode(CharSequence rawPassword) {
return JBCRYPT_HEADER + JBCRYPT_ENCODER.encode(rawPassword);
return getPasswordHeader() + PASSWORD_HASH_ENCODER.encode(rawPassword);
}

@Override
public boolean matches(CharSequence rawPassword, String encPass) {
if (isPasswordHashed(encPass)) {
return JBCRYPT_ENCODER.matches(rawPassword, encPass.substring(JBCRYPT_HEADER.length()));
return PASSWORD_HASH_ENCODER.matches(rawPassword, encPass.substring(getPasswordHeader().length()));
} else {
return false;
}
Expand All @@ -965,9 +1079,18 @@ public boolean isPasswordHashed(String password) {
if (password == null) {
return false;
}
return password.startsWith(JBCRYPT_HEADER) && JBCRYPT_ENCODER.isHashValid(password.substring(JBCRYPT_HEADER.length()));
if (password.startsWith(getPasswordHeader())) {
return PASSWORD_HASH_ENCODER.isHashValid(password.substring(getPasswordHeader().length()));
}
if (password.startsWith(FIPS140.useCompliantAlgorithms() ? JBCRYPT : PBKDF2)) {
// switch the header to see if this is using a different encryption
LOGGER.log(Level.WARNING, "A password appears to be stored (or is attempting to be stored) that was created with a different"
+ " hashing/encryption algorithm, check the FIPS-140 state of the system has not changed inadvertently");
} else {
LOGGER.log(Level.FINE, "A password appears to be stored (or is attempting to be stored) that is not hashed/encrypted.");
}
return false;
}

}

public static final MultiPasswordEncoder PASSWORD_ENCODER = new MultiPasswordEncoder();
Expand Down
Loading

0 comments on commit f37e1b7

Please sign in to comment.