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}