Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package plexus-sec-dispatcher4 for
openSUSE:Factory checked in at 2026-11-06 19:58:44
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/plexus-sec-dispatcher4 (Old)
and /work/SRC/openSUSE:Factory/.plexus-sec-dispatcher4.new.3759290 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "plexus-sec-dispatcher4"
Fri Nov 6 19:58:44 2026 rev:4 rq:1395845 version:4.3.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/plexus-sec-dispatcher4/plexus-sec-dispatcher4.changes
2026-09-28 10:39:33.219607982 +0200
+++
/work/SRC/openSUSE:Factory/.plexus-sec-dispatcher4.new.3759290/plexus-sec-dispatcher4.changes
2026-11-06 20:06:51.845590007 +0100
@@ -1,0 +2,23 @@
+Fri Nov 6 10:44:58 UTC 2026 - Fridrich Strba <[email protected]>
+
+- Update to version 4.3.0
+ * New features and improvements
+ + Try to remove sensitive parts from memory more aggressively
+ + Add Git Credential Helper as master password source
+ * Documentation updates
+ + Stop pinning a version in the README snippet
+ * Maintenance
+ + Clean up declared dependencies
+ * Build
+ + Point the site URL at the published site so site:stage works
+ + Allow running the release drafter by hand
+ * Dependency updates
+ + Bump org.eclipse.sisu:org.eclipse.sisu.inject from 1.1.0 to
+ 1.1.1
+ + Bump release-drafter/release-drafter from 7.7.0 to 7.10.0
+ + Bump version.slf4j from 2.0.18 to 2.0.20
+ + Bump org.codehaus.modello:modello-maven-plugin from 2.8.0 to
+ 2.8.1
+ + Bump github/codeql-action from 4.37.7 to 4.37.8
+
+-------------------------------------------------------------------
Old:
----
sec-dispatcher-4.2.0.tar.gz
New:
----
sec-dispatcher-4.3.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ plexus-sec-dispatcher4.spec ++++++
--- /var/tmp/diff_new_pack.iw1Rc9/_old 2026-11-06 20:06:52.995638202 +0100
+++ /var/tmp/diff_new_pack.iw1Rc9/_new 2026-11-06 20:06:52.998638328 +0100
@@ -15,18 +15,19 @@
# Please submit bugfixes or comments via https://bugs.opensuse.org/
#
+
%global fragment_name sec-dispatcher
%global base_name plexus-%{fragment_name}
%global version_suffix 4
Name: %{base_name}%{version_suffix}
-Version: 4.2.0
+Version: 4.3.0
Release: 0
Summary: Plexus Security Dispatcher Component
License: Apache-2.0
Group: Development/Libraries/Java
URL: https://github.com/codehaus-plexus/%{base_name}
Source0:
https://github.com/codehaus-plexus/plexus-sec-dispatcher/archive/refs/tags/%{fragment_name}-%{version}.tar.gz
-Source1: http://www.apache.org/licenses/LICENSE-2.0.txt
+Source1: https://www.apache.org/licenses/LICENSE-2.0.txt
Source100: %{name}-build.xml
BuildRequires: ant
BuildRequires: atinject
++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.iw1Rc9/_old 2026-11-06 20:06:53.113643147 +0100
+++ /var/tmp/diff_new_pack.iw1Rc9/_new 2026-11-06 20:06:53.124643608 +0100
@@ -1,6 +1,6 @@
-mtime: 1790293737
-commit: 55fe35cb379d9d790d590f942ee5a1e2c79408d2670a964f98470b2fea43cae1
+mtime: 1793962123
+commit: 34ad73c25001af1e3e21c9246b2ff32301cf607b6218db8404e97db4dbaca196
url: https://src.opensuse.org/java-packages/plexus-sec-dispatcher4
-revision: 55fe35cb379d9d790d590f942ee5a1e2c79408d2670a964f98470b2fea43cae1
+revision: 34ad73c25001af1e3e21c9246b2ff32301cf607b6218db8404e97db4dbaca196
projectscmsync: https://src.opensuse.org/java-packages/_ObsPrj
++++++ build.specials.obscpio ++++++
++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore 1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore 2026-11-06 11:48:43.000000000 +0100
@@ -0,0 +1 @@
+.osc
++++++ plexus-sec-dispatcher4-build.xml ++++++
--- /var/tmp/diff_new_pack.iw1Rc9/_old 2026-11-06 20:06:53.463657815 +0100
+++ /var/tmp/diff_new_pack.iw1Rc9/_new 2026-11-06 20:06:53.469658067 +0100
@@ -10,7 +10,7 @@
<property name="project.groupId" value="org.codehaus.plexus"/>
<property name="project.artifactId" value="plexus-sec-dispatcher"/>
- <property name="project.version" value="4.2.0"/>
+ <property name="project.version" value="4.3.0"/>
<property name="project.url"
value="https://github.com/codehaus-plexus/plexus-sec-dispatcher"/>
<property name="project.name" value="Plexus Security Dispatcher Component"/>
<property name="project.organization.name" value="Sonatype, Inc."/>
++++++ sec-dispatcher-4.2.0.tar.gz -> sec-dispatcher-4.3.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/.github/workflows/codeql.yml
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/.github/workflows/codeql.yml
--- old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/.github/workflows/codeql.yml
2026-08-19 21:22:50.000000000 +0200
+++ new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/.github/workflows/codeql.yml
2026-11-06 11:38:06.000000000 +0100
@@ -59,7 +59,7 @@
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/[email protected]
+ uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -87,6 +87,6 @@
exit 1
- name: Perform CodeQL Analysis
- uses: github/codeql-action/[email protected]
+ uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/.github/workflows/maven.yml
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/.github/workflows/maven.yml
--- old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/.github/workflows/maven.yml
2026-08-19 21:22:50.000000000 +0200
+++ new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/.github/workflows/maven.yml
2026-11-06 11:38:06.000000000 +0100
@@ -17,7 +17,7 @@
name: GitHub CI
-on: [push, pull_request]
+on: [push, pull_request, workflow_dispatch]
jobs:
build:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/.github/workflows/release-drafter.yml
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/.github/workflows/release-drafter.yml
---
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/.github/workflows/release-drafter.yml
2026-08-19 21:22:50.000000000 +0200
+++
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/.github/workflows/release-drafter.yml
2026-11-06 11:38:06.000000000 +0100
@@ -3,10 +3,11 @@
push:
branches:
- master
+ workflow_dispatch:
jobs:
update_release_draft:
runs-on: ubuntu-latest
steps:
- - uses: release-drafter/[email protected]
+ - uses: release-drafter/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/README.md
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/README.md
--- old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/README.md 2026-08-19
21:22:50.000000000 +0200
+++ new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/README.md 2026-11-06
11:38:06.000000000 +0100
@@ -23,7 +23,6 @@
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-sec-dispatcher</artifactId>
- <version>4.1.0</version>
</dependency>
```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/pom.xml
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/pom.xml
--- old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/pom.xml 2026-08-19
21:22:50.000000000 +0200
+++ new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/pom.xml 2026-11-06
11:38:06.000000000 +0100
@@ -10,7 +10,7 @@
</parent>
<artifactId>plexus-sec-dispatcher</artifactId>
- <version>4.2.0</version>
+ <version>4.3.0</version>
<name>Plexus Security Dispatcher Component</name>
<description>This library provides encryption/decryption functionality with
pluggable ciphers and password providers</description>
@@ -18,7 +18,7 @@
<scm>
<connection>scm:git:https://github.com/codehaus-plexus/plexus-sec-dispatcher.git</connection>
<developerConnection>${project.scm.connection}</developerConnection>
- <tag>sec-dispatcher-4.2.0</tag>
+ <tag>sec-dispatcher-4.3.0</tag>
<url>https://github.com/codehaus-plexus/plexus-sec-dispatcher/tree/${project.scm.tag}/</url>
</scm>
@@ -30,16 +30,17 @@
<distributionManagement>
<site>
<id>github:gh-pages</id>
- <url>${project.scm.developerConnection}</url>
+ <!-- the published site; scm-publish takes its target from pubScmUrl,
and site:deploy is skipped -->
+ <url>https://codehaus-plexus.github.io/plexus-sec-dispatcher/</url>
</site>
</distributionManagement>
<properties>
<javaVersion>17</javaVersion>
-
<project.build.outputTimestamp>2026-08-19T19:22:39Z</project.build.outputTimestamp>
+
<project.build.outputTimestamp>2026-11-06T10:37:51Z</project.build.outputTimestamp>
- <version.sisu>1.1.0</version.sisu>
- <version.slf4j>2.0.18</version.slf4j>
+ <version.sisu>1.1.1</version.sisu>
+ <version.slf4j>2.0.20</version.slf4j>
</properties>
<dependencies>
@@ -55,12 +56,6 @@
<version>1</version>
<scope>provided</scope>
</dependency>
- <dependency>
- <groupId>org.eclipse.sisu</groupId>
- <artifactId>org.eclipse.sisu.inject</artifactId>
- <version>${version.sisu}</version>
- <scope>provided</scope>
- </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
@@ -113,7 +108,7 @@
<plugin>
<groupId>org.codehaus.modello</groupId>
<artifactId>modello-maven-plugin</artifactId>
- <version>2.8.0</version>
+ <version>2.8.1</version>
<configuration>
<version>4.0.0</version>
<models>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPadding.java
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPadding.java
---
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPadding.java
2026-08-19 21:22:50.000000000 +0200
+++
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPadding.java
2026-11-06 11:38:06.000000000 +0100
@@ -27,13 +27,14 @@
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Named;
import javax.inject.Singleton;
+import javax.security.auth.DestroyFailedException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
-import java.security.spec.KeySpec;
+import java.util.Arrays;
import java.util.Base64;
import org.codehaus.plexus.components.secdispatcher.CipherException;
@@ -57,15 +58,19 @@
byte[] salt = getRandomNonce(SALT_LENGTH_BYTE);
byte[] iv = getRandomNonce(IV_LENGTH_BYTE);
SecretKey secretKey =
getAESKeyFromPassword(password.toCharArray(), salt);
- Cipher cipher = Cipher.getInstance(CIPHER_ALG);
- cipher.init(Cipher.ENCRYPT_MODE, secretKey, new
GCMParameterSpec(TAG_LENGTH_BIT, iv));
- byte[] cipherText =
cipher.doFinal(clearText.getBytes(StandardCharsets.UTF_8));
- byte[] cipherTextWithIvSalt = ByteBuffer.allocate(iv.length +
salt.length + cipherText.length)
- .put(iv)
- .put(salt)
- .put(cipherText)
- .array();
- return Base64.getEncoder().encodeToString(cipherTextWithIvSalt);
+ try {
+ Cipher cipher = Cipher.getInstance(CIPHER_ALG);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new
GCMParameterSpec(TAG_LENGTH_BIT, iv));
+ byte[] cipherText =
cipher.doFinal(clearText.getBytes(StandardCharsets.UTF_8));
+ byte[] cipherTextWithIvSalt = ByteBuffer.allocate(iv.length +
salt.length + cipherText.length)
+ .put(iv)
+ .put(salt)
+ .put(cipherText)
+ .array();
+ return
Base64.getEncoder().encodeToString(cipherTextWithIvSalt);
+ } finally {
+ destroyQuietly(secretKey);
+ }
} catch (Exception e) {
throw new CipherException("Failed encrypting", e);
}
@@ -83,10 +88,14 @@
byte[] cipherText = new byte[buffer.remaining()];
buffer.get(cipherText);
SecretKey secretKey =
getAESKeyFromPassword(password.toCharArray(), salt);
- Cipher cipher = Cipher.getInstance(CIPHER_ALG);
- cipher.init(Cipher.DECRYPT_MODE, secretKey, new
GCMParameterSpec(TAG_LENGTH_BIT, iv));
- byte[] plainText = cipher.doFinal(cipherText);
- return new String(plainText, StandardCharsets.UTF_8);
+ try {
+ Cipher cipher = Cipher.getInstance(CIPHER_ALG);
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new
GCMParameterSpec(TAG_LENGTH_BIT, iv));
+ byte[] plainText = cipher.doFinal(cipherText);
+ return new String(plainText, StandardCharsets.UTF_8);
+ } finally {
+ destroyQuietly(secretKey);
+ }
} catch (Exception e) {
throw new CipherException("Failed decrypting", e);
}
@@ -100,8 +109,36 @@
private static SecretKey getAESKeyFromPassword(char[] password, byte[]
salt)
throws NoSuchAlgorithmException, InvalidKeySpecException {
- SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_FACTORY);
- KeySpec spec = new PBEKeySpec(password, salt, PBE_ITERATIONS,
PBE_KEY_SIZE);
- return new SecretKeySpec(factory.generateSecret(spec).getEncoded(),
KEY_ALGORITHM);
+ PBEKeySpec spec = new PBEKeySpec(password, salt, PBE_ITERATIONS,
PBE_KEY_SIZE);
+ try {
+ SecretKeyFactory factory =
SecretKeyFactory.getInstance(KEY_FACTORY);
+ SecretKey originalKey = factory.generateSecret(spec);
+ byte[] encodedKey = originalKey.getEncoded();
+ try {
+ return new SecretKeySpec(encodedKey, KEY_ALGORITHM);
+ } finally {
+ destroyQuietly(encodedKey);
+ destroyQuietly(originalKey);
+ }
+ } finally {
+ spec.clearPassword();
+ // also clear the passed password array to avoid leaving sensitive
data in memory
+ Arrays.fill(password, '\0');
+ }
+ }
+
+ private static void destroyQuietly(byte[] data) {
+ if (data != null) {
+ Arrays.fill(data, (byte) 0x00);
+ }
+ }
+
+ private static void destroyQuietly(SecretKey key) {
+ try {
+ key.destroy();
+ } catch (DestroyFailedException e) {
+ // Ignore exception during key destruction as not all SecretKey
implementations support destruction
+ // https://bugs.openjdk.org/browse/JDK-8389121
+ }
}
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSource.java
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSource.java
---
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSource.java
1970-01-01 01:00:00.000000000 +0100
+++
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSource.java
2026-11-06 11:38:06.000000000 +0100
@@ -0,0 +1,267 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.plexus.components.secdispatcher.internal.sources;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
+
+/**
+ * Password source that uses Git Credential Helpers.
+ * <p>
+ * Git credential helpers have a common interface for retrieving credentials.
+ * This master source allows using any git credential helper to retrieve
passwords.
+ * <p>
+ * Config: {@code git-credential:helper-name?url=protocol://host/path}
+ * <p>
+ * Examples:
+ * <ul>
+ * <li>{@code git-credential:cache?url=https://maven.apache.org/master}</li>
+ * <li>{@code git-credential:store?url=https://maven.apache.org/master}</li>
+ * <li>{@code
git-credential:/usr/local/bin/git-credential-osxkeychain?url=https://maven.apache.org/master}</li>
+ * </ul>
+ *
+ * @see <a href="https://git-scm.com/docs/gitcredentials">Git Credentials</a>
+ * @see <a href="https://git-scm.com/doc/credential-helpers">Git Credential
Helpers</a>
+ */
+@Singleton
+@Named(GitCredentialHelperMasterSource.NAME)
+public final class GitCredentialHelperMasterSource extends
PrefixMasterSourceSupport implements MasterSourceMeta {
+ public static final String NAME = "git-credential";
+
+ public GitCredentialHelperMasterSource() {
+ super(NAME + ":");
+ }
+
+ @Override
+ public String description() {
+ return "Git Credential Helper (helper name and URL should be edited)";
+ }
+
+ @Override
+ public Optional<String> configTemplate() {
+ return Optional.of(NAME + ":helper-name?url=protocol://host/path");
+ }
+
+ @Override
+ protected String doHandle(String transformed) throws
SecDispatcherException {
+ String helperName;
+ String url;
+
+ // Parse configuration: helper-name?url=protocol://host/path
+ int queryIndex = transformed.indexOf('?');
+ if (queryIndex < 0) {
+ throw new SecDispatcherException(
+ "Invalid git-credential configuration. Expected format:
git-credential:helper-name?url=protocol://host/path");
+ }
+
+ helperName = transformed.substring(0, queryIndex);
+ String query = transformed.substring(queryIndex + 1);
+
+ if (!query.startsWith("url=")) {
+ throw new SecDispatcherException(
+ "Invalid git-credential configuration. Expected URL
parameter: url=protocol://host/path");
+ }
+
+ url = query.substring(4);
+
+ try {
+ return retrievePassword(helperName, url);
+ } catch (IOException | InterruptedException e) {
+ throw new SecDispatcherException(
+ String.format(
+ "Failed to retrieve password from git credential
helper '%s': %s",
+ helperName, e.getMessage()),
+ e);
+ }
+ }
+
+ @Override
+ protected SecDispatcher.ValidationResponse doValidateConfiguration(String
transformed) {
+ HashMap<SecDispatcher.ValidationResponse.Level, List<String>> report =
new HashMap<>();
+ boolean isValid = false;
+
+ try {
+ int queryIndex = transformed.indexOf('?');
+ if (queryIndex < 0) {
+
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new
ArrayList<>())
+ .add(
+ "Invalid configuration format. Expected:
git-credential:helper-name?url=protocol://host/path");
+ return new
SecDispatcher.ValidationResponse(getClass().getSimpleName(), false, report,
List.of());
+ }
+
+ String helperName = transformed.substring(0, queryIndex);
+ String query = transformed.substring(queryIndex + 1);
+
+ if (!query.startsWith("url=")) {
+
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new
ArrayList<>())
+ .add("Invalid configuration. Expected URL parameter:
url=protocol://host/path");
+ return new
SecDispatcher.ValidationResponse(getClass().getSimpleName(), false, report,
List.of());
+ }
+
+ String url = query.substring(4);
+
+ // Validate URL format
+ try {
+ new URI(url);
+ } catch (URISyntaxException e) {
+
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new
ArrayList<>())
+ .add(String.format("Invalid URL format: %s",
e.getMessage()));
+ return new
SecDispatcher.ValidationResponse(getClass().getSimpleName(), false, report,
List.of());
+ }
+
+ // Try to execute the helper to see if it's available
+ String helperCommand = buildHelperCommand(helperName);
+ try {
+ Process process = new ProcessBuilder(helperCommand,
"get").start();
+ // Close stdin to prevent the helper from waiting for input
+ process.getOutputStream().close();
+
+ if (!process.waitFor(2, TimeUnit.SECONDS)) {
+ process.destroyForcibly();
+
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.WARNING, k -> new
ArrayList<>())
+ .add(String.format(
+ "Git credential helper '%s' did not
respond in time. It may still work.",
+ helperName));
+ isValid = true; // Still consider it valid, just warn
+ } else if (process.exitValue() == 127 || process.exitValue()
== 126) {
+
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new
ArrayList<>())
+ .add(String.format("Git credential helper '%s' not
found or not executable", helperName));
+ } else {
+
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new
ArrayList<>())
+ .add(String.format("Git credential helper '%s' is
available", helperName));
+ isValid = true;
+ }
+ } catch (IOException e) {
+
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new
ArrayList<>())
+ .add(String.format(
+ "Failed to execute git credential helper '%s':
%s", helperName, e.getMessage()));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new
ArrayList<>())
+ .add("Validation was interrupted");
+ }
+ } catch (Exception e) {
+
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new
ArrayList<>())
+ .add(String.format("Validation error: %s",
e.getMessage()));
+ }
+
+ return new
SecDispatcher.ValidationResponse(getClass().getSimpleName(), isValid, report,
List.of());
+ }
+
+ private String retrievePassword(String helperName, String url) throws
IOException, InterruptedException {
+ String helperCommand = buildHelperCommand(helperName);
+
+ ProcessBuilder pb = new ProcessBuilder(helperCommand, "get");
+ Process process = pb.start();
+
+ // Write credential request to helper's stdin
+ try (PrintWriter writer = new PrintWriter(new
OutputStreamWriter(process.getOutputStream()))) {
+ URI uri = new URI(url);
+ if (uri.getScheme() != null) {
+ writer.println("protocol=" + uri.getScheme());
+ }
+ if (uri.getHost() != null && !uri.getHost().isEmpty()) {
+ if (uri.getPort() != -1) {
+ writer.println("host=" + uri.getHost() + ":" +
uri.getPort());
+ } else {
+ writer.println("host=" + uri.getHost());
+ }
+ }
+ if (uri.getPath() != null && !uri.getPath().isEmpty()) {
+ writer.println("path=" + uri.getPath());
+ }
+ writer.println(); // Blank line signals end of input
+ writer.flush();
+ } catch (URISyntaxException e) {
+ throw new IOException("Invalid URL format: " + e.getMessage(), e);
+ }
+
+ // Read response from helper's stdout
+ String password = null;
+ try (BufferedReader reader = new BufferedReader(new
InputStreamReader(process.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (line.startsWith("password=")) {
+ password = line.substring(9);
+ break;
+ }
+ }
+ }
+
+ if (!process.waitFor(30, TimeUnit.SECONDS)) {
+ process.destroyForcibly();
+ throw new IOException("Git credential helper timed out");
+ }
+
+ int exitCode = process.exitValue();
+ if (exitCode != 0) {
+ String errorOutput = readProcessError(process);
+ throw new IOException(
+ String.format("Git credential helper exited with code %d.
Error: %s", exitCode, errorOutput));
+ }
+
+ if (password == null || password.isEmpty()) {
+ throw new IOException("Git credential helper did not return a
password");
+ }
+
+ return password;
+ }
+
+ static String buildHelperCommand(String helperName) {
+ // If helper name contains a path separator, use it as-is (absolute or
relative path)
+ // Otherwise, prefix with "git-credential-"
+ if (helperName.contains("/") || helperName.contains("\\")) {
+ return helperName;
+ }
+ return "git-credential-" + helperName;
+ }
+
+ private String readProcessError(Process process) {
+ try (BufferedReader reader = new BufferedReader(new
InputStreamReader(process.getErrorStream()))) {
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (sb.length() > 0) {
+ sb.append("; ");
+ }
+ sb.append(line);
+ }
+ return sb.toString();
+ } catch (IOException e) {
+ return "(failed to read error output)";
+ }
+ }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSourceTest.java
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSourceTest.java
---
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSourceTest.java
1970-01-01 01:00:00.000000000 +0100
+++
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSourceTest.java
2026-11-06 11:38:06.000000000 +0100
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.plexus.components.secdispatcher.internal.sources;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class GitCredentialHelperMasterSourceTest {
+
+ @TempDir
+ static Path tempDir;
+
+ static Path mockHelperPath;
+
+ @BeforeAll
+ static void setup() throws IOException {
+ // Create a mock git credential helper script
+ // On Windows, create a batch file; on Unix-like systems, create a
shell script
+ if (System.getProperty("os.name").toLowerCase().contains("win")) {
+ mockHelperPath = tempDir.resolve("mock-git-credential-helper.bat");
+ String batchScript = "@echo off\r\n"
+ + "if \"%1\"==\"get\" (\r\n"
+ + " REM Read input until empty line\r\n"
+ + " :loop\r\n"
+ + " set /p line=\r\n"
+ + " if not defined line goto output\r\n"
+ + " goto loop\r\n"
+ + " :output\r\n"
+ + " echo protocol=https\r\n"
+ + " echo host=maven.apache.org\r\n"
+ + " echo username=testuser\r\n"
+ + " echo password=testPassword123\r\n"
+ + ")\r\n";
+ Files.writeString(mockHelperPath, batchScript);
+ } else {
+ mockHelperPath = tempDir.resolve("mock-git-credential-helper");
+ String script = "#!/bin/sh\n"
+ + "if [ \"$1\" = \"get\" ]; then\n"
+ + " # Read input (we don't actually parse it in this
simple mock)\n"
+ + " while IFS= read -r line; do\n"
+ + " [ -z \"$line\" ] && break\n"
+ + " done\n"
+ + " # Return mock credentials\n"
+ + " echo \"protocol=https\"\n"
+ + " echo \"host=maven.apache.org\"\n"
+ + " echo \"username=testuser\"\n"
+ + " echo \"password=testPassword123\"\n"
+ + "fi\n";
+
+ Files.writeString(mockHelperPath, script);
+ // Make it executable on Unix-like systems
+ Files.setPosixFilePermissions(
+ mockHelperPath,
+ Set.of(
+ PosixFilePermission.OWNER_READ,
+ PosixFilePermission.OWNER_WRITE,
+ PosixFilePermission.OWNER_EXECUTE));
+ }
+ }
+
+ @Test
+ void testMetadata() {
+ GitCredentialHelperMasterSource source = new
GitCredentialHelperMasterSource();
+
+ assertNotNull(source.description());
+ assertTrue(source.description().contains("Git Credential Helper"));
+
+ assertTrue(source.configTemplate().isPresent());
+ assertEquals(
+ "git-credential:helper-name?url=protocol://host/path",
+ source.configTemplate().get());
+ }
+
+ @Test
+ void testHandleReturnsNullForNonMatchingPrefix() throws
SecDispatcherException {
+ GitCredentialHelperMasterSource source = new
GitCredentialHelperMasterSource();
+
+ assertNull(source.handle("env:SOME_VAR"));
+ assertNull(source.handle("file:/path/to/file"));
+ assertNull(source.handle("other-prefix:value"));
+ }
+
+ @Test
+ void testHandleThrowsExceptionForMissingUrlParameter() {
+ GitCredentialHelperMasterSource source = new
GitCredentialHelperMasterSource();
+
+ SecDispatcherException exception =
+ assertThrows(SecDispatcherException.class, () ->
source.handle("git-credential:helper-name"));
+
+ assertTrue(exception.getMessage().contains("Expected format"));
+ }
+
+ @Test
+ void testHandleThrowsExceptionForInvalidUrlParameter() {
+ GitCredentialHelperMasterSource source = new
GitCredentialHelperMasterSource();
+
+ SecDispatcherException exception = assertThrows(
+ SecDispatcherException.class, () ->
source.handle("git-credential:helper-name?invalid=value"));
+
+ assertTrue(exception.getMessage().contains("Expected URL parameter"));
+ }
+
+ @Test
+ @DisabledOnOs(
+ value = OS.WINDOWS,
+ disabledReason = "Windows batch files don't handle closed stdin
gracefully in validation tests")
+ void testHandleWithMockHelper() throws SecDispatcherException {
+ GitCredentialHelperMasterSource source = new
GitCredentialHelperMasterSource();
+
+ String config = mockHelperPath.toString() +
"?url=https://maven.apache.org/master";
+ String password = source.handle("git-credential:" + config);
+
+ assertEquals("testPassword123", password);
+ }
+
+ @Test
+ void testValidateConfigurationWithInvalidFormat() {
+ GitCredentialHelperMasterSource source = new
GitCredentialHelperMasterSource();
+
+ SecDispatcher.ValidationResponse response =
source.validateConfiguration("git-credential:invalid-format");
+
+ assertNotNull(response);
+ assertFalse(response.isValid());
+
assertTrue(response.getReport().containsKey(SecDispatcher.ValidationResponse.Level.ERROR));
+ }
+
+ @Test
+ void testValidateConfigurationWithInvalidUrl() {
+ GitCredentialHelperMasterSource source = new
GitCredentialHelperMasterSource();
+
+ SecDispatcher.ValidationResponse response =
+
source.validateConfiguration("git-credential:helper?url=invalid url with
spaces");
+
+ assertNotNull(response);
+ assertFalse(response.isValid());
+
assertTrue(response.getReport().containsKey(SecDispatcher.ValidationResponse.Level.ERROR));
+ }
+
+ @Test
+ void testValidateConfigurationWithNonMatchingPrefix() {
+ GitCredentialHelperMasterSource source = new
GitCredentialHelperMasterSource();
+
+ SecDispatcher.ValidationResponse response =
source.validateConfiguration("env:SOME_VAR");
+
+ assertNull(response);
+ }
+
+ @Test
+ @DisabledOnOs(
+ value = OS.WINDOWS,
+ disabledReason = "Windows batch files don't handle closed stdin
gracefully in validation tests")
+ void testValidateConfigurationWithMockHelper() {
+ GitCredentialHelperMasterSource source = new
GitCredentialHelperMasterSource();
+
+ String config = mockHelperPath.toString() +
"?url=https://maven.apache.org/master";
+ SecDispatcher.ValidationResponse response =
source.validateConfiguration("git-credential:" + config);
+
+ assertNotNull(response);
+ assertTrue(response.isValid());
+
assertTrue(response.getReport().containsKey(SecDispatcher.ValidationResponse.Level.INFO));
+ }
+
+ @Test
+ void testBuildHelperCommandWithShortName() {
+ String result =
GitCredentialHelperMasterSource.buildHelperCommand("cache");
+ assertEquals("git-credential-cache", result);
+
+ result = GitCredentialHelperMasterSource.buildHelperCommand("store");
+ assertEquals("git-credential-store", result);
+ }
+
+ @Test
+ void testBuildHelperCommandWithPath() {
+ String result =
GitCredentialHelperMasterSource.buildHelperCommand("/usr/local/bin/git-credential-osxkeychain");
+ assertEquals("/usr/local/bin/git-credential-osxkeychain", result);
+
+ result =
GitCredentialHelperMasterSource.buildHelperCommand("./relative/path/helper");
+ assertEquals("./relative/path/helper", result);
+ }
+}