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