001/** 002 * Copyright (C) 2006-2018 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")).map(File::new).orElseGet( 098 () -> new File(System.getProperty("user.home"), ".m2")); 099 } 100 101 public static void main(final String[] args) { 102 System.out.println(new MavenDecrypter().find(args[0])); 103 } 104 105 private static class MvnServerExtractor extends DefaultHandler { 106 107 private static final Pattern ENCRYPTED_PATTERN = Pattern.compile(".*?[^\\\\]?\\{(.*?[^\\\\])\\}.*"); 108 109 private final String passphrase; 110 111 private final String serverId; 112 113 private Server server; 114 115 private String encryptedPassword; 116 117 private boolean done; 118 119 private StringBuilder current; 120 121 private MvnServerExtractor(final String passphrase, final String serverId) { 122 this.passphrase = doDecrypt(passphrase, "settings.security"); 123 this.serverId = serverId; 124 } 125 126 @Override 127 public void startElement(final String uri, final String localName, final String qName, 128 final Attributes attributes) { 129 if ("server".equalsIgnoreCase(qName)) { 130 if (!done) { 131 server = new Server(); 132 } 133 } else if (server != null) { 134 current = new StringBuilder(); 135 } 136 } 137 138 @Override 139 public void characters(final char[] ch, final int start, final int length) { 140 if (current != null) { 141 current.append(new String(ch, start, length)); 142 } 143 } 144 145 @Override 146 public void endElement(final String uri, final String localName, final String qName) { 147 if (done) { 148 // decrypt password only when the server is found 149 server.setPassword(doDecrypt(encryptedPassword, passphrase)); 150 return; 151 } 152 if ("server".equalsIgnoreCase(qName)) { 153 if (server.getId().equals(serverId)) { 154 done = true; 155 } else if (!done) { 156 server = null; 157 encryptedPassword = null; 158 } 159 } else if (server != null && current != null) { 160 switch (qName) { 161 case "id": 162 server.setId(current.toString()); 163 break; 164 case "username": 165 try { 166 server.setUsername(doDecrypt(current.toString(), passphrase)); 167 } catch (final RuntimeException re) { 168 server.setUsername(current.toString()); 169 } 170 break; 171 case "password": 172 encryptedPassword = current.toString(); 173 break; 174 default: 175 } 176 current = null; 177 } 178 } 179 180 private String doDecrypt(final String value, final String pwd) { 181 if (value == null) { 182 return null; 183 } 184 185 final Matcher matcher = ENCRYPTED_PATTERN.matcher(value); 186 if (!matcher.matches() && !matcher.find()) { 187 return value; // not encrypted, just use it 188 } 189 190 final String bare = matcher.group(1); 191 if (value.startsWith("${env.")) { 192 final String key = bare.substring("env.".length()); 193 return ofNullable(System.getenv(key)).orElseGet(() -> System.getProperty(bare)); 194 } 195 if (value.startsWith("${")) { // all is system prop, no interpolation yet 196 return System.getProperty(bare); 197 } 198 199 if (pwd == null || pwd.isEmpty()) { 200 throw new IllegalArgumentException("Master password can't be null or empty."); 201 } 202 203 if (bare.contains("[") && bare.contains("]") && bare.contains("type=")) { 204 throw new IllegalArgumentException("Unsupported encryption for " + value); 205 } 206 207 final byte[] allEncryptedBytes = Base64.getMimeDecoder().decode(bare); 208 final int totalLen = allEncryptedBytes.length; 209 final byte[] salt = new byte[8]; 210 System.arraycopy(allEncryptedBytes, 0, salt, 0, 8); 211 final byte padLen = allEncryptedBytes[8]; 212 final byte[] encryptedBytes = new byte[totalLen - 8 - 1 - padLen]; 213 System.arraycopy(allEncryptedBytes, 8 + 1, encryptedBytes, 0, encryptedBytes.length); 214 215 try { 216 final MessageDigest digest = MessageDigest.getInstance("SHA-256"); 217 byte[] keyAndIv = new byte[16 * 2]; 218 byte[] result; 219 int currentPos = 0; 220 221 while (currentPos < keyAndIv.length) { 222 digest.update(pwd.getBytes(StandardCharsets.UTF_8)); 223 224 digest.update(salt, 0, 8); 225 result = digest.digest(); 226 227 final int stillNeed = keyAndIv.length - currentPos; 228 if (result.length > stillNeed) { 229 final byte[] b = new byte[stillNeed]; 230 System.arraycopy(result, 0, b, 0, b.length); 231 result = b; 232 } 233 234 System.arraycopy(result, 0, keyAndIv, currentPos, result.length); 235 236 currentPos += result.length; 237 if (currentPos < keyAndIv.length) { 238 digest.reset(); 239 digest.update(result); 240 } 241 } 242 243 final byte[] key = new byte[16]; 244 final byte[] iv = new byte[16]; 245 System.arraycopy(keyAndIv, 0, key, 0, key.length); 246 System.arraycopy(keyAndIv, key.length, iv, 0, iv.length); 247 248 final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 249 cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv)); 250 251 final byte[] clearBytes = cipher.doFinal(encryptedBytes); 252 return new String(clearBytes, StandardCharsets.UTF_8); 253 } catch (final Exception e) { 254 throw new IllegalStateException(e); 255 } 256 } 257 } 258 259 private static class MvnMasterExtractor extends DefaultHandler { 260 261 private StringBuilder current; 262 263 @Override 264 public void startElement(final String uri, final String localName, final String qName, 265 final Attributes attributes) { 266 if ("master".equalsIgnoreCase(qName)) { 267 current = new StringBuilder(); 268 } 269 } 270 271 @Override 272 public void characters(final char[] ch, final int start, final int length) { 273 if (current != null) { 274 current.append(new String(ch, start, length)); 275 } 276 } 277 } 278}