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