001/** 002 * Copyright (C) 2006-2026 Talend Inc. - www.talend.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.talend.sdk.component.maven; 017 018import static java.util.Optional.ofNullable; 019 020import java.io.File; 021import java.io.FileInputStream; 022import java.io.IOException; 023import java.io.InputStream; 024import java.nio.charset.StandardCharsets; 025import java.security.MessageDigest; 026import java.util.Base64; 027import java.util.Collections; 028import java.util.List; 029import java.util.Objects; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032import java.util.stream.Stream; 033 034import javax.crypto.Cipher; 035import javax.crypto.spec.IvParameterSpec; 036import javax.crypto.spec.SecretKeySpec; 037import javax.xml.XMLConstants; 038import javax.xml.parsers.ParserConfigurationException; 039import javax.xml.parsers.SAXParser; 040import javax.xml.parsers.SAXParserFactory; 041 042import org.xml.sax.Attributes; 043import org.xml.sax.SAXException; 044import org.xml.sax.SAXNotRecognizedException; 045import org.xml.sax.SAXNotSupportedException; 046import org.xml.sax.helpers.DefaultHandler; 047 048import lombok.EqualsAndHashCode; 049import lombok.ToString; 050 051@ToString 052@EqualsAndHashCode 053public class MavenDecrypter { 054 055 private static final String M2_HOME = "M2_HOME"; 056 057 private static final String MAVEN_HOME = "MAVEN_HOME"; 058 059 private static final String USER_HOME = "user.home"; 060 061 private static final String FILE_SETTINGS = "settings.xml"; 062 063 private static final String FILE_SECURITY = "settings-security.xml"; 064 065 private final List<File> settings; 066 067 private final File settingsSecurity; 068 069 public MavenDecrypter() { 070 this(findSettingsFiles(), new File(getM2(), FILE_SECURITY)); 071 } 072 073 @Deprecated 074 public MavenDecrypter(final File settings, final File settingsSecurity) { 075 this(Collections.singletonList(settings), settingsSecurity); 076 } 077 078 public MavenDecrypter(final List<File> settings, final File settingsSecurity) { 079 this.settings = settings.stream().filter(File::exists).toList(); 080 this.settingsSecurity = settingsSecurity; 081 } 082 083 public Server find(final String serverId) { 084 final SAXParser parser = newSaxParser(); 085 if (settings.isEmpty()) { 086 throw new IllegalArgumentException( 087 "No " + settings + " found, ensure your credentials configuration is valid"); 088 } 089 090 final String master; 091 if (settingsSecurity.isFile()) { 092 final MvnMasterExtractor extractor = new MvnMasterExtractor(); 093 try (final InputStream is = new FileInputStream(settingsSecurity)) { 094 parser.parse(is, extractor); 095 } catch (final IOException | SAXException e) { 096 throw new IllegalArgumentException(e); 097 } 098 master = extractor.current == null ? null : extractor.current.toString().trim(); 099 } else { 100 master = null; 101 } 102 103 final MvnServerExtractor extractor = new MvnServerExtractor(master, serverId); 104 for (final File file : settings) { 105 try (final InputStream is = new FileInputStream(file)) { 106 parser.parse(is, extractor); 107 } catch (final IOException | SAXException e) { 108 throw new IllegalArgumentException(e); 109 } 110 if (extractor.server != null) { 111 return extractor.server; 112 } 113 } 114 115 throw new IllegalArgumentException("Didn't find " + serverId + " in " + settings); 116 } 117 118 private static SAXParser newSaxParser() { 119 final SAXParserFactory factory = SAXParserFactory.newInstance(); 120 factory.setNamespaceAware(false); 121 factory.setValidating(false); 122 try { 123 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 124 factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE); 125 } catch (final ParserConfigurationException | SAXNotRecognizedException | SAXNotSupportedException ex) { 126 // ignore 127 } 128 129 final SAXParser parser; 130 try { 131 parser = factory.newSAXParser(); 132 } catch (final ParserConfigurationException | SAXException e) { 133 throw new IllegalStateException(e); 134 } 135 136 try { 137 parser.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 138 parser.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); 139 } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { 140 // ignore 141 } 142 143 return parser; 144 } 145 146 private static File getM2() { 147 return ofNullable(System.getProperty("talend.maven.decrypter.m2.location")) 148 .map(File::new) 149 .orElseGet(() -> new File(System.getProperty(USER_HOME), ".m2")); 150 } 151 152 private static List<File> findSettingsFiles() { 153 return Stream.of( 154 new File(getM2(), FILE_SETTINGS), 155 findMavenHome(M2_HOME), 156 findMavenHome(MAVEN_HOME)) 157 .filter(Objects::nonNull) 158 .toList(); 159 } 160 161 private static File findMavenHome(final String mavenHome) { 162 return ofNullable(System.getenv(mavenHome)) 163 .map(File::new) 164 .map(it -> new File(it, "conf/" + FILE_SETTINGS)) 165 .orElse(null); 166 } 167 168 public static void main(final String[] args) { 169 System.out.println(new MavenDecrypter().find(args[0])); 170 } 171 172 private static class MvnServerExtractor extends DefaultHandler { 173 174 private static final Pattern ENCRYPTED_PATTERN = Pattern.compile(".*?[^\\\\]?\\{(.*?[^\\\\])\\}.*"); 175 176 private final String passphrase; 177 178 private final String serverId; 179 180 private Server server; 181 182 private String encryptedPassword; 183 184 private boolean done; 185 186 private StringBuilder current; 187 188 private MvnServerExtractor(final String passphrase, final String serverId) { 189 this.passphrase = doDecrypt(passphrase, "settings.security"); 190 this.serverId = serverId; 191 } 192 193 @Override 194 public void startElement(final String uri, final String localName, final String qName, 195 final Attributes attributes) { 196 if ("server".equalsIgnoreCase(qName)) { 197 if (!done) { 198 server = new Server(); 199 } 200 } else if (server != null) { 201 current = new StringBuilder(); 202 } 203 } 204 205 @Override 206 public void characters(final char[] ch, final int start, final int length) { 207 if (current != null) { 208 current.append(new String(ch, start, length)); 209 } 210 } 211 212 @Override 213 public void endElement(final String uri, final String localName, final String qName) { 214 if (done) { 215 // decrypt password only when the server is found 216 server.setPassword(doDecrypt(encryptedPassword, passphrase)); 217 return; 218 } 219 if ("server".equalsIgnoreCase(qName)) { 220 if (server.getId().equals(serverId)) { 221 done = true; 222 } else if (!done) { 223 server = null; 224 encryptedPassword = null; 225 } 226 } else if (server != null && current != null) { 227 switch (qName) { 228 case "id": 229 server.setId(current.toString()); 230 break; 231 case "username": 232 try { 233 server.setUsername(doDecrypt(current.toString(), passphrase)); 234 } catch (final RuntimeException re) { 235 server.setUsername(current.toString()); 236 } 237 break; 238 case "password": 239 encryptedPassword = current.toString(); 240 break; 241 default: 242 } 243 current = null; 244 } 245 } 246 247 private String doDecrypt(final String value, final String pwd) { 248 if (value == null) { 249 return null; 250 } 251 252 final Matcher matcher = ENCRYPTED_PATTERN.matcher(value); 253 if (!matcher.matches() && !matcher.find()) { 254 return value; // not encrypted, just use it 255 } 256 257 final String bare = matcher.group(1); 258 if (value.startsWith("${env.")) { 259 final String key = bare.substring("env.".length()); 260 return ofNullable(System.getenv(key)).orElseGet(() -> System.getProperty(bare)); 261 } 262 if (value.startsWith("${")) { // all is system prop, no interpolation yet 263 return System.getProperty(bare); 264 } 265 266 if (pwd == null || pwd.isEmpty()) { 267 throw new IllegalArgumentException("Master password can't be null or empty."); 268 } 269 270 if (bare.contains("[") && bare.contains("]") && bare.contains("type=")) { 271 throw new IllegalArgumentException("Unsupported encryption for " + value); 272 } 273 274 final byte[] allEncryptedBytes = Base64.getMimeDecoder().decode(bare); 275 final int totalLen = allEncryptedBytes.length; 276 final byte[] salt = new byte[8]; 277 System.arraycopy(allEncryptedBytes, 0, salt, 0, 8); 278 final byte padLen = allEncryptedBytes[8]; 279 final byte[] encryptedBytes = new byte[totalLen - 8 - 1 - padLen]; 280 System.arraycopy(allEncryptedBytes, 8 + 1, encryptedBytes, 0, encryptedBytes.length); 281 282 try { 283 final MessageDigest digest = MessageDigest.getInstance("SHA-256"); 284 byte[] keyAndIv = new byte[16 * 2]; 285 byte[] result; 286 int currentPos = 0; 287 288 while (currentPos < keyAndIv.length) { 289 digest.update(pwd.getBytes(StandardCharsets.UTF_8)); 290 291 digest.update(salt, 0, 8); 292 result = digest.digest(); 293 294 final int stillNeed = keyAndIv.length - currentPos; 295 if (result.length > stillNeed) { 296 final byte[] b = new byte[stillNeed]; 297 System.arraycopy(result, 0, b, 0, b.length); 298 result = b; 299 } 300 301 System.arraycopy(result, 0, keyAndIv, currentPos, result.length); 302 303 currentPos += result.length; 304 if (currentPos < keyAndIv.length) { 305 digest.reset(); 306 digest.update(result); 307 } 308 } 309 310 final byte[] key = new byte[16]; 311 final byte[] iv = new byte[16]; 312 System.arraycopy(keyAndIv, 0, key, 0, key.length); 313 System.arraycopy(keyAndIv, key.length, iv, 0, iv.length); 314 315 final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 316 cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv)); 317 318 final byte[] clearBytes = cipher.doFinal(encryptedBytes); 319 return new String(clearBytes, StandardCharsets.UTF_8); 320 } catch (final Exception e) { 321 throw new IllegalStateException(e); 322 } 323 } 324 } 325 326 private static class MvnMasterExtractor extends DefaultHandler { 327 328 private StringBuilder current; 329 330 @Override 331 public void startElement(final String uri, final String localName, final String qName, 332 final Attributes attributes) { 333 if ("master".equalsIgnoreCase(qName)) { 334 current = new StringBuilder(); 335 } 336 } 337 338 @Override 339 public void characters(final char[] ch, final int start, final int length) { 340 if (current != null) { 341 current.append(new String(ch, start, length)); 342 } 343 } 344 } 345}