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}