diff --git a/pom.xml b/pom.xml
index 05bfac45..1d49615f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,7 +67,7 @@
999999-SNAPSHOT
jenkinsci/${project.artifactId}-plugin
- 2.426.3
+ 2.462.3
1372
diff --git a/src/main/java/com/cloudbees/plugins/credentials/SecretBytesReactionExtension.java b/src/main/java/com/cloudbees/plugins/credentials/SecretBytesReactionExtension.java
new file mode 100644
index 00000000..01ac3dbe
--- /dev/null
+++ b/src/main/java/com/cloudbees/plugins/credentials/SecretBytesReactionExtension.java
@@ -0,0 +1,25 @@
+package com.cloudbees.plugins.credentials;
+
+import hudson.ExtensionList;
+import hudson.init.Initializer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jenkins.security.ExtendedReadRedaction;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+@Restricted(NoExternalUse.class)
+public class SecretBytesReactionExtension {
+
+ public static final Logger LOGGER = Logger.getLogger(SecretBytesReactionExtension.class.getName());
+
+ // TODO Delete this and annotate `SecretBytesRedaction` with `@Extension` once the core dependency is >= 2.479
+ @Initializer
+ public static void create() {
+ try {
+ ExtensionList.lookup(ExtendedReadRedaction.class).add(new SecretBytesRedaction());
+ } catch (NoClassDefFoundError unused) {
+ LOGGER.log(Level.WARNING, "Failed to register SecretBytesRedaction. Update Jenkins to add support for redacting credentials in config.xml files from users with ExtendedRead permission. Learn more: https://www.jenkins.io/redirect/plugin/credentials/SecretBytesRedaction/");
+ }
+ }
+}
diff --git a/src/main/java/com/cloudbees/plugins/credentials/SecretBytesRedaction.java b/src/main/java/com/cloudbees/plugins/credentials/SecretBytesRedaction.java
new file mode 100644
index 00000000..2f3a0a89
--- /dev/null
+++ b/src/main/java/com/cloudbees/plugins/credentials/SecretBytesRedaction.java
@@ -0,0 +1,27 @@
+package com.cloudbees.plugins.credentials;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import jenkins.security.ExtendedReadRedaction;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+@Restricted(NoExternalUse.class)
+// @Extension
+// See SecretBytesReactionExtension
+public class SecretBytesRedaction implements ExtendedReadRedaction {
+ private static final Pattern SECRET_BYTES_PATTERN = Pattern.compile(">(" + SecretBytes.ENCRYPTED_VALUE_PATTERN + ")<");
+
+ @Override
+ public String apply(String configDotXml) {
+ Matcher matcher = SECRET_BYTES_PATTERN.matcher(configDotXml);
+ StringBuilder cleanXml = new StringBuilder();
+ while (matcher.find()) {
+ if (SecretBytes.isSecretBytes(matcher.group(1))) {
+ matcher.appendReplacement(cleanXml, ">********<");
+ }
+ }
+ matcher.appendTail(cleanXml);
+ return cleanXml.toString();
+ }
+}
diff --git a/src/test/java/com/cloudbees/plugins/credentials/SecretBytesRedactionTest.java b/src/test/java/com/cloudbees/plugins/credentials/SecretBytesRedactionTest.java
new file mode 100644
index 00000000..f11c2b18
--- /dev/null
+++ b/src/test/java/com/cloudbees/plugins/credentials/SecretBytesRedactionTest.java
@@ -0,0 +1,78 @@
+package com.cloudbees.plugins.credentials;
+
+import com.cloudbees.hudson.plugins.folder.Folder;
+import com.cloudbees.plugins.credentials.domains.Domain;
+import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
+import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
+import hudson.model.Item;
+import hudson.model.ModelObject;
+import java.util.Base64;
+import java.util.Iterator;
+import jenkins.model.Jenkins;
+import org.htmlunit.Page;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.MockAuthorizationStrategy;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class SecretBytesRedactionTest {
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @Test
+ public void testRedaction() throws Exception {
+ final String usernamePasswordPassword = "thisisthe_theuserpassword";
+ final SecretBytes secretBytes = SecretBytes.fromString("thisis_theTestData");
+
+ Item.EXTENDED_READ.setEnabled(true);
+
+ final Folder folder = j.jenkins.createProject(Folder.class, "F");
+ final CredentialsStore store = lookupStore(folder);
+ final UsernamePasswordCredentialsImpl usernamePasswordCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "passwordid", null, "theusername", usernamePasswordPassword);
+ store.addCredentials(Domain.global(), usernamePasswordCredentials);
+ store.addCredentials(Domain.global(), new SecretBytesCredential(CredentialsScope.GLOBAL, "certid", "thedesc", secretBytes));
+
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("alice").grant(Item.READ, Item.EXTENDED_READ, Jenkins.READ).everywhere().to("bob"));
+
+ try (JenkinsRule.WebClient webClient = j.createWebClient().login("alice")) {
+ final Page page = webClient.goTo("job/F/config.xml", "application/xml");
+ final String content = page.getWebResponse().getContentAsString();
+ assertThat(content, containsString(usernamePasswordCredentials.getPassword().getEncryptedValue()));
+ assertThat(content, containsString(Base64.getEncoder().encodeToString(secretBytes.getEncryptedData())));
+ }
+ try (JenkinsRule.WebClient webClient = j.createWebClient().login("bob")) {
+ final Page page = webClient.goTo("job/F/config.xml", "application/xml");
+ final String content = page.getWebResponse().getContentAsString();
+ assertThat(content, not(containsString(usernamePasswordCredentials.getPassword().getEncryptedValue())));
+ assertThat(content, not(containsString(Base64.getEncoder().encodeToString(secretBytes.getEncryptedData()))));
+ assertThat(content, containsString("********"));
+ assertThat(content, containsString("********"));
+ }
+ }
+
+ // Stolen from BaseStandardCredentialsTest
+ private static CredentialsStore lookupStore(ModelObject object) {
+ Iterator stores = CredentialsProvider.lookupStores(object).iterator();
+ assertTrue(stores.hasNext());
+ CredentialsStore store = stores.next();
+ assertEquals("we got the expected store", object, store.getContext());
+ return store;
+ }
+
+ // This would be nicer with a real credential like `FileCredentialsImpl` but another test falls over if we add `plain-credentials` to the test scope
+ public static class SecretBytesCredential extends BaseStandardCredentials {
+ private final SecretBytes mySecretBytes;
+
+ public SecretBytesCredential(CredentialsScope scope, String id, String description, SecretBytes bytes) {
+ super(scope, id, description);
+ this.mySecretBytes = bytes;
+ }
+ }
+}