Skip to content

Commit 55f1275

Browse files
authored
Merge pull request #269 from Kevin-CB/add-base64-masking
Add base64 masking
2 parents b19cfc6 + d685136 commit 55f1275

File tree

4 files changed

+271
-0
lines changed

4 files changed

+271
-0
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package org.jenkinsci.plugins.credentialsbinding.masking;
2+
3+
import edu.umd.cs.findbugs.annotations.NonNull;
4+
import hudson.Extension;
5+
import java.nio.charset.StandardCharsets;
6+
import java.util.ArrayList;
7+
import java.util.Base64;
8+
import java.util.Collection;
9+
import java.util.Collections;
10+
import org.kohsuke.accmod.Restricted;
11+
import org.kohsuke.accmod.restrictions.NoExternalUse;
12+
13+
@Extension
14+
@Restricted(NoExternalUse.class)
15+
public class Base64SecretPatternFactory implements SecretPatternFactory {
16+
@NonNull
17+
@Override
18+
public Collection<String> getEncodedForms(@NonNull String input) {
19+
return getBase64Forms(input);
20+
}
21+
22+
@NonNull
23+
public Collection<String> getBase64Forms(@NonNull String secret) {
24+
if (secret.length() == 0) {
25+
return Collections.emptyList();
26+
}
27+
28+
Base64.Encoder[] encoders = new Base64.Encoder[]{
29+
Base64.getEncoder(),
30+
Base64.getUrlEncoder(),
31+
};
32+
33+
Collection<String> result = new ArrayList<>();
34+
String[] shifts = {"", "a", "aa"};
35+
36+
for (String shift : shifts) {
37+
for (Base64.Encoder encoder : encoders) {
38+
String shiftedSecret = shift + secret;
39+
String encoded = encoder.encodeToString(shiftedSecret.getBytes(StandardCharsets.UTF_8));
40+
String processedEncoded = shift.length() > 0 ? encoded.substring(2 * shift.length()) : encoded;
41+
result.add(processedEncoded);
42+
result.add(removeTrailingEquals(processedEncoded));
43+
}
44+
}
45+
return result;
46+
}
47+
48+
private String removeTrailingEquals(String base64Value) {
49+
if (base64Value.endsWith("==")) {
50+
// removing the last 3 characters, the character before the == being incomplete
51+
return base64Value.substring(0, base64Value.length() - 3);
52+
}
53+
if (base64Value.endsWith("=")) {
54+
// removing the last 2 characters, the character before the = being incomplete
55+
return base64Value.substring(0, base64Value.length() - 2);
56+
}
57+
return base64Value;
58+
}
59+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package org.jenkinsci.plugins.credentialsbinding.masking;
2+
3+
import static org.hamcrest.Matchers.is;
4+
import static org.jenkinsci.plugins.credentialsbinding.test.Executables.executable;
5+
import static org.junit.Assume.assumeThat;
6+
7+
import hudson.Functions;
8+
import org.jenkinsci.plugins.credentialsbinding.test.CredentialsTestUtil;
9+
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
10+
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
11+
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
12+
import org.junit.Rule;
13+
import org.junit.Test;
14+
import org.jvnet.hudson.test.JenkinsRule;
15+
16+
public class Base64SecretPatternFactoryTest {
17+
18+
@Rule
19+
public JenkinsRule j = new JenkinsRule();
20+
21+
public static final String SAMPLE_PASSWORD = "}#T14'GAz&H!{$U_";
22+
23+
@Test
24+
public void base64SecretsAreMaskedInLogs() throws Exception {
25+
WorkflowJob project = j.createProject(WorkflowJob.class);
26+
String credentialsId = CredentialsTestUtil.registerUsernamePasswordCredentials(j.jenkins, "user", SAMPLE_PASSWORD);
27+
String script;
28+
29+
if (Functions.isWindows()) {
30+
assumeThat("powershell", is(executable()));
31+
script =
32+
" powershell '''\n"
33+
+ " $secret = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(\"$env:PASSWORD\"))\n"
34+
+ " echo $secret\n"
35+
+ " '''\n";
36+
} else {
37+
script =
38+
" sh '''\n"
39+
+ " echo -n $PASSWORD | base64\n"
40+
+ " '''\n";
41+
}
42+
43+
project.setDefinition(new CpsFlowDefinition(
44+
"node {\n"
45+
+ " withCredentials([usernamePassword(credentialsId: '" + credentialsId + "', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {\n"
46+
+ script
47+
+ " }\n"
48+
+ "}", true));
49+
50+
WorkflowRun run = j.assertBuildStatusSuccess(project.scheduleBuild2(0));
51+
52+
j.assertLogContains("****", run);
53+
j.assertLogNotContains(SAMPLE_PASSWORD, run);
54+
}
55+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package org.jenkinsci.plugins.credentialsbinding.test;
2+
3+
import org.jenkinsci.plugins.credentialsbinding.masking.Base64SecretPatternFactory;
4+
import org.junit.Assert;
5+
import org.junit.Test;
6+
7+
import java.nio.charset.StandardCharsets;
8+
import java.util.Base64;
9+
import java.util.Collection;
10+
11+
public class Base64PatternTest {
12+
@Test
13+
public void checkSecretDetected() {
14+
assertBase64PatternFound("abcde", "abcde");
15+
assertBase64PatternFound("abcde", "1abcde");
16+
assertBase64PatternFound("abcde", "12abcde");
17+
assertBase64PatternFound("abcde", "123abcde");
18+
assertBase64PatternFound("abcde", "abcde1");
19+
assertBase64PatternFound("abcde", "abcde12");
20+
assertBase64PatternFound("abcde", "abcde123");
21+
assertBase64PatternFound("abcde", "1abcde1");
22+
assertBase64PatternFound("abcde", "1abcde12");
23+
assertBase64PatternFound("abcde", "1abcde123");
24+
assertBase64PatternFound("abcde", "12abcde1");
25+
assertBase64PatternFound("abcde", "12abcde12");
26+
assertBase64PatternFound("abcde", "12abcde123");
27+
assertBase64PatternFound("abcde", "123abcde1");
28+
assertBase64PatternFound("abcde", "123abcde12");
29+
assertBase64PatternFound("abcde", "123abcde123");
30+
31+
assertBase64PatternFound("abcd", "abcde");
32+
assertBase64PatternFound("abcd", "1abcde");
33+
assertBase64PatternFound("abcd", "12abcde");
34+
assertBase64PatternFound("abcd", "123abcde");
35+
assertBase64PatternFound("abcd", "abcde1");
36+
assertBase64PatternFound("abcd", "abcde12");
37+
assertBase64PatternFound("abcd", "abcde123");
38+
assertBase64PatternFound("abcd", "1abcde1");
39+
assertBase64PatternFound("abcd", "1abcde12");
40+
assertBase64PatternFound("abcd", "1abcde123");
41+
assertBase64PatternFound("abcd", "12abcde1");
42+
assertBase64PatternFound("abcd", "12abcde12");
43+
assertBase64PatternFound("abcd", "12abcde123");
44+
assertBase64PatternFound("abcd", "123abcde1");
45+
assertBase64PatternFound("abcd", "123abcde12");
46+
assertBase64PatternFound("abcd", "123abcde123");
47+
48+
assertBase64PatternFound("bcd", "abcde");
49+
assertBase64PatternFound("bcd", "1abcde");
50+
assertBase64PatternFound("bcd", "12abcde");
51+
assertBase64PatternFound("bcd", "123abcde");
52+
assertBase64PatternFound("bcd", "abcde1");
53+
assertBase64PatternFound("bcd", "abcde12");
54+
assertBase64PatternFound("bcd", "abcde123");
55+
assertBase64PatternFound("bcd", "1abcde1");
56+
assertBase64PatternFound("bcd", "1abcde12");
57+
assertBase64PatternFound("bcd", "1abcde123");
58+
assertBase64PatternFound("bcd", "12abcde1");
59+
assertBase64PatternFound("bcd", "12abcde12");
60+
assertBase64PatternFound("bcd", "12abcde123");
61+
assertBase64PatternFound("bcd", "123abcde1");
62+
assertBase64PatternFound("bcd", "123abcde12");
63+
assertBase64PatternFound("bcd", "123abcde123");
64+
}
65+
66+
@Test
67+
public void checkSecretNotDetected() {
68+
assertBase64PatternNotFound("ab1cde", "abcde");
69+
assertBase64PatternNotFound("ab1cde", "1abcde");
70+
assertBase64PatternNotFound("ab1cde", "12abcde");
71+
assertBase64PatternNotFound("ab1cde", "123abcde");
72+
assertBase64PatternNotFound("ab1cde", "abcde1");
73+
assertBase64PatternNotFound("ab1cde", "abcde12");
74+
assertBase64PatternNotFound("ab1cde", "abcde123");
75+
assertBase64PatternNotFound("ab1cde", "1abcde1");
76+
assertBase64PatternNotFound("ab1cde", "1abcde12");
77+
assertBase64PatternNotFound("ab1cde", "1abcde123");
78+
assertBase64PatternNotFound("ab1cde", "12abcde1");
79+
assertBase64PatternNotFound("ab1cde", "12abcde12");
80+
assertBase64PatternNotFound("ab1cde", "12abcde123");
81+
assertBase64PatternNotFound("ab1cde", "123abcde1");
82+
assertBase64PatternNotFound("ab1cde", "123abcde12");
83+
assertBase64PatternNotFound("ab1cde", "123abcde123");
84+
85+
assertBase64PatternNotFound("ab1cd", "abcde");
86+
assertBase64PatternNotFound("ab1cd", "1abcde");
87+
assertBase64PatternNotFound("ab1cd", "12abcde");
88+
assertBase64PatternNotFound("ab1cd", "123abcde");
89+
assertBase64PatternNotFound("ab1cd", "abcde1");
90+
assertBase64PatternNotFound("ab1cd", "abcde12");
91+
assertBase64PatternNotFound("ab1cd", "abcde123");
92+
assertBase64PatternNotFound("ab1cd", "1abcde1");
93+
assertBase64PatternNotFound("ab1cd", "1abcde12");
94+
assertBase64PatternNotFound("ab1cd", "1abcde123");
95+
assertBase64PatternNotFound("ab1cd", "12abcde1");
96+
assertBase64PatternNotFound("ab1cd", "12abcde12");
97+
assertBase64PatternNotFound("ab1cd", "12abcde123");
98+
assertBase64PatternNotFound("ab1cd", "123abcde1");
99+
assertBase64PatternNotFound("ab1cd", "123abcde12");
100+
assertBase64PatternNotFound("ab1cd", "123abcde123");
101+
102+
assertBase64PatternNotFound("b1cd", "abcde");
103+
assertBase64PatternNotFound("b1cd", "1abcde");
104+
assertBase64PatternNotFound("b1cd", "12abcde");
105+
assertBase64PatternNotFound("b1cd", "123abcde");
106+
assertBase64PatternNotFound("b1cd", "abcde1");
107+
assertBase64PatternNotFound("b1cd", "abcde12");
108+
assertBase64PatternNotFound("b1cd", "abcde123");
109+
assertBase64PatternNotFound("b1cd", "1abcde1");
110+
assertBase64PatternNotFound("b1cd", "1abcde12");
111+
assertBase64PatternNotFound("b1cd", "1abcde123");
112+
assertBase64PatternNotFound("b1cd", "12abcde1");
113+
assertBase64PatternNotFound("b1cd", "12abcde12");
114+
assertBase64PatternNotFound("b1cd", "12abcde123");
115+
assertBase64PatternNotFound("b1cd", "123abcde1");
116+
assertBase64PatternNotFound("b1cd", "123abcde12");
117+
assertBase64PatternNotFound("b1cd", "123abcde123");
118+
}
119+
120+
private void assertBase64PatternFound(String secret, String plainText) {
121+
Assert.assertTrue("Pattern " + plainText + " not detected as containing " + secret, isPatternContainingSecret(secret, plainText));
122+
}
123+
124+
private void assertBase64PatternNotFound(String secret, String plainText) {
125+
Assert.assertFalse("Pattern " + plainText + " was detected as containing " + secret, isPatternContainingSecret(secret, plainText));
126+
}
127+
128+
public boolean isPatternContainingSecret(String secret, String plainText) {
129+
Base64SecretPatternFactory factory = new Base64SecretPatternFactory();
130+
Collection<String> allPatterns = factory.getBase64Forms(secret);
131+
132+
String base64Text = Base64.getEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8));
133+
134+
return allPatterns.stream().anyMatch(base64Text::contains);
135+
}
136+
}

src/test/java/org/jenkinsci/plugins/credentialsbinding/test/CredentialsTestUtil.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626

2727
import com.cloudbees.plugins.credentials.CredentialsProvider;
2828
import com.cloudbees.plugins.credentials.CredentialsScope;
29+
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
2930
import com.cloudbees.plugins.credentials.domains.Domain;
31+
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
3032
import hudson.model.ModelObject;
3133
import hudson.util.Secret;
3234
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
@@ -55,4 +57,23 @@ public static void setStringCredentials(ModelObject context, String credentialsI
5557
StringCredentials creds = new StringCredentialsImpl(CredentialsScope.GLOBAL, credentialsId, null, Secret.fromString(value));
5658
CredentialsProvider.lookupStores(context).iterator().next().addCredentials(Domain.global(), creds);
5759
}
60+
61+
/**
62+
* Registers the given value as a {@link UsernamePasswordCredentials} into the default {@link CredentialsProvider}.
63+
* Returns the generated credential id for the registered credentials.
64+
*/
65+
public static String registerUsernamePasswordCredentials(ModelObject context, String username, String password) throws IOException {
66+
String credentialsId = UUID.randomUUID().toString();
67+
setUsernamePasswordCredentials(context, credentialsId, username, password);
68+
return credentialsId;
69+
}
70+
71+
/**
72+
* Registers the given value as a {@link UsernamePasswordCredentials} into the default {@link CredentialsProvider} using the
73+
* specified credentials id.
74+
*/
75+
public static void setUsernamePasswordCredentials(ModelObject context, String credentialsId, String username, String password) throws IOException {
76+
UsernamePasswordCredentials creds = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credentialsId, null, username, password);
77+
CredentialsProvider.lookupStores(context).iterator().next().addCredentials(Domain.global(), creds);
78+
}
5879
}

0 commit comments

Comments
 (0)