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