Skip to content

Commit

Permalink
Merge pull request #269 from Kevin-CB/add-base64-masking
Browse files Browse the repository at this point in the history
Add base64 masking
  • Loading branch information
jglick authored Sep 14, 2023
2 parents b19cfc6 + d685136 commit 55f1275
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.jenkinsci.plugins.credentialsbinding.masking;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

@Extension
@Restricted(NoExternalUse.class)
public class Base64SecretPatternFactory implements SecretPatternFactory {
@NonNull
@Override
public Collection<String> getEncodedForms(@NonNull String input) {
return getBase64Forms(input);
}

@NonNull
public Collection<String> getBase64Forms(@NonNull String secret) {
if (secret.length() == 0) {
return Collections.emptyList();
}

Base64.Encoder[] encoders = new Base64.Encoder[]{
Base64.getEncoder(),
Base64.getUrlEncoder(),
};

Collection<String> result = new ArrayList<>();
String[] shifts = {"", "a", "aa"};

for (String shift : shifts) {
for (Base64.Encoder encoder : encoders) {
String shiftedSecret = shift + secret;
String encoded = encoder.encodeToString(shiftedSecret.getBytes(StandardCharsets.UTF_8));
String processedEncoded = shift.length() > 0 ? encoded.substring(2 * shift.length()) : encoded;
result.add(processedEncoded);
result.add(removeTrailingEquals(processedEncoded));
}
}
return result;
}

private String removeTrailingEquals(String base64Value) {
if (base64Value.endsWith("==")) {
// removing the last 3 characters, the character before the == being incomplete
return base64Value.substring(0, base64Value.length() - 3);
}
if (base64Value.endsWith("=")) {
// removing the last 2 characters, the character before the = being incomplete
return base64Value.substring(0, base64Value.length() - 2);
}
return base64Value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.jenkinsci.plugins.credentialsbinding.masking;

import static org.hamcrest.Matchers.is;
import static org.jenkinsci.plugins.credentialsbinding.test.Executables.executable;
import static org.junit.Assume.assumeThat;

import hudson.Functions;
import org.jenkinsci.plugins.credentialsbinding.test.CredentialsTestUtil;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;

public class Base64SecretPatternFactoryTest {

@Rule
public JenkinsRule j = new JenkinsRule();

public static final String SAMPLE_PASSWORD = "}#T14'GAz&H!{$U_";

@Test
public void base64SecretsAreMaskedInLogs() throws Exception {
WorkflowJob project = j.createProject(WorkflowJob.class);
String credentialsId = CredentialsTestUtil.registerUsernamePasswordCredentials(j.jenkins, "user", SAMPLE_PASSWORD);
String script;

if (Functions.isWindows()) {
assumeThat("powershell", is(executable()));
script =
" powershell '''\n"
+ " $secret = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(\"$env:PASSWORD\"))\n"
+ " echo $secret\n"
+ " '''\n";
} else {
script =
" sh '''\n"
+ " echo -n $PASSWORD | base64\n"
+ " '''\n";
}

project.setDefinition(new CpsFlowDefinition(
"node {\n"
+ " withCredentials([usernamePassword(credentialsId: '" + credentialsId + "', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {\n"
+ script
+ " }\n"
+ "}", true));

WorkflowRun run = j.assertBuildStatusSuccess(project.scheduleBuild2(0));

j.assertLogContains("****", run);
j.assertLogNotContains(SAMPLE_PASSWORD, run);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package org.jenkinsci.plugins.credentialsbinding.test;

import org.jenkinsci.plugins.credentialsbinding.masking.Base64SecretPatternFactory;
import org.junit.Assert;
import org.junit.Test;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collection;

public class Base64PatternTest {
@Test
public void checkSecretDetected() {
assertBase64PatternFound("abcde", "abcde");
assertBase64PatternFound("abcde", "1abcde");
assertBase64PatternFound("abcde", "12abcde");
assertBase64PatternFound("abcde", "123abcde");
assertBase64PatternFound("abcde", "abcde1");
assertBase64PatternFound("abcde", "abcde12");
assertBase64PatternFound("abcde", "abcde123");
assertBase64PatternFound("abcde", "1abcde1");
assertBase64PatternFound("abcde", "1abcde12");
assertBase64PatternFound("abcde", "1abcde123");
assertBase64PatternFound("abcde", "12abcde1");
assertBase64PatternFound("abcde", "12abcde12");
assertBase64PatternFound("abcde", "12abcde123");
assertBase64PatternFound("abcde", "123abcde1");
assertBase64PatternFound("abcde", "123abcde12");
assertBase64PatternFound("abcde", "123abcde123");

assertBase64PatternFound("abcd", "abcde");
assertBase64PatternFound("abcd", "1abcde");
assertBase64PatternFound("abcd", "12abcde");
assertBase64PatternFound("abcd", "123abcde");
assertBase64PatternFound("abcd", "abcde1");
assertBase64PatternFound("abcd", "abcde12");
assertBase64PatternFound("abcd", "abcde123");
assertBase64PatternFound("abcd", "1abcde1");
assertBase64PatternFound("abcd", "1abcde12");
assertBase64PatternFound("abcd", "1abcde123");
assertBase64PatternFound("abcd", "12abcde1");
assertBase64PatternFound("abcd", "12abcde12");
assertBase64PatternFound("abcd", "12abcde123");
assertBase64PatternFound("abcd", "123abcde1");
assertBase64PatternFound("abcd", "123abcde12");
assertBase64PatternFound("abcd", "123abcde123");

assertBase64PatternFound("bcd", "abcde");
assertBase64PatternFound("bcd", "1abcde");
assertBase64PatternFound("bcd", "12abcde");
assertBase64PatternFound("bcd", "123abcde");
assertBase64PatternFound("bcd", "abcde1");
assertBase64PatternFound("bcd", "abcde12");
assertBase64PatternFound("bcd", "abcde123");
assertBase64PatternFound("bcd", "1abcde1");
assertBase64PatternFound("bcd", "1abcde12");
assertBase64PatternFound("bcd", "1abcde123");
assertBase64PatternFound("bcd", "12abcde1");
assertBase64PatternFound("bcd", "12abcde12");
assertBase64PatternFound("bcd", "12abcde123");
assertBase64PatternFound("bcd", "123abcde1");
assertBase64PatternFound("bcd", "123abcde12");
assertBase64PatternFound("bcd", "123abcde123");
}

@Test
public void checkSecretNotDetected() {
assertBase64PatternNotFound("ab1cde", "abcde");
assertBase64PatternNotFound("ab1cde", "1abcde");
assertBase64PatternNotFound("ab1cde", "12abcde");
assertBase64PatternNotFound("ab1cde", "123abcde");
assertBase64PatternNotFound("ab1cde", "abcde1");
assertBase64PatternNotFound("ab1cde", "abcde12");
assertBase64PatternNotFound("ab1cde", "abcde123");
assertBase64PatternNotFound("ab1cde", "1abcde1");
assertBase64PatternNotFound("ab1cde", "1abcde12");
assertBase64PatternNotFound("ab1cde", "1abcde123");
assertBase64PatternNotFound("ab1cde", "12abcde1");
assertBase64PatternNotFound("ab1cde", "12abcde12");
assertBase64PatternNotFound("ab1cde", "12abcde123");
assertBase64PatternNotFound("ab1cde", "123abcde1");
assertBase64PatternNotFound("ab1cde", "123abcde12");
assertBase64PatternNotFound("ab1cde", "123abcde123");

assertBase64PatternNotFound("ab1cd", "abcde");
assertBase64PatternNotFound("ab1cd", "1abcde");
assertBase64PatternNotFound("ab1cd", "12abcde");
assertBase64PatternNotFound("ab1cd", "123abcde");
assertBase64PatternNotFound("ab1cd", "abcde1");
assertBase64PatternNotFound("ab1cd", "abcde12");
assertBase64PatternNotFound("ab1cd", "abcde123");
assertBase64PatternNotFound("ab1cd", "1abcde1");
assertBase64PatternNotFound("ab1cd", "1abcde12");
assertBase64PatternNotFound("ab1cd", "1abcde123");
assertBase64PatternNotFound("ab1cd", "12abcde1");
assertBase64PatternNotFound("ab1cd", "12abcde12");
assertBase64PatternNotFound("ab1cd", "12abcde123");
assertBase64PatternNotFound("ab1cd", "123abcde1");
assertBase64PatternNotFound("ab1cd", "123abcde12");
assertBase64PatternNotFound("ab1cd", "123abcde123");

assertBase64PatternNotFound("b1cd", "abcde");
assertBase64PatternNotFound("b1cd", "1abcde");
assertBase64PatternNotFound("b1cd", "12abcde");
assertBase64PatternNotFound("b1cd", "123abcde");
assertBase64PatternNotFound("b1cd", "abcde1");
assertBase64PatternNotFound("b1cd", "abcde12");
assertBase64PatternNotFound("b1cd", "abcde123");
assertBase64PatternNotFound("b1cd", "1abcde1");
assertBase64PatternNotFound("b1cd", "1abcde12");
assertBase64PatternNotFound("b1cd", "1abcde123");
assertBase64PatternNotFound("b1cd", "12abcde1");
assertBase64PatternNotFound("b1cd", "12abcde12");
assertBase64PatternNotFound("b1cd", "12abcde123");
assertBase64PatternNotFound("b1cd", "123abcde1");
assertBase64PatternNotFound("b1cd", "123abcde12");
assertBase64PatternNotFound("b1cd", "123abcde123");
}

private void assertBase64PatternFound(String secret, String plainText) {
Assert.assertTrue("Pattern " + plainText + " not detected as containing " + secret, isPatternContainingSecret(secret, plainText));
}

private void assertBase64PatternNotFound(String secret, String plainText) {
Assert.assertFalse("Pattern " + plainText + " was detected as containing " + secret, isPatternContainingSecret(secret, plainText));
}

public boolean isPatternContainingSecret(String secret, String plainText) {
Base64SecretPatternFactory factory = new Base64SecretPatternFactory();
Collection<String> allPatterns = factory.getBase64Forms(secret);

String base64Text = Base64.getEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8));

return allPatterns.stream().anyMatch(base64Text::contains);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@

import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
import hudson.model.ModelObject;
import hudson.util.Secret;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
Expand Down Expand Up @@ -55,4 +57,23 @@ public static void setStringCredentials(ModelObject context, String credentialsI
StringCredentials creds = new StringCredentialsImpl(CredentialsScope.GLOBAL, credentialsId, null, Secret.fromString(value));
CredentialsProvider.lookupStores(context).iterator().next().addCredentials(Domain.global(), creds);
}

/**
* Registers the given value as a {@link UsernamePasswordCredentials} into the default {@link CredentialsProvider}.
* Returns the generated credential id for the registered credentials.
*/
public static String registerUsernamePasswordCredentials(ModelObject context, String username, String password) throws IOException {
String credentialsId = UUID.randomUUID().toString();
setUsernamePasswordCredentials(context, credentialsId, username, password);
return credentialsId;
}

/**
* Registers the given value as a {@link UsernamePasswordCredentials} into the default {@link CredentialsProvider} using the
* specified credentials id.
*/
public static void setUsernamePasswordCredentials(ModelObject context, String credentialsId, String username, String password) throws IOException {
UsernamePasswordCredentials creds = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credentialsId, null, username, password);
CredentialsProvider.lookupStores(context).iterator().next().addCredentials(Domain.global(), creds);
}
}

0 comments on commit 55f1275

Please sign in to comment.