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}