/* * Copyright 2016 ThoughtWorks, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.thoughtworks.go.config; import com.thoughtworks.go.security.CipherProvider; import com.thoughtworks.go.security.GoCipher; import com.thoughtworks.go.util.SystemEnvironment; import com.thoughtworks.go.util.TimeProvider; import com.thoughtworks.go.util.XmlUtils; import org.apache.commons.io.FileUtils; import org.bouncycastle.crypto.InvalidCipherTextException; import org.jdom2.Attribute; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.filter.Filters; import org.jdom2.input.SAXBuilder; import org.jdom2.xpath.XPathExpression; import org.jdom2.xpath.XPathFactory; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.List; import static com.thoughtworks.go.util.ExceptionUtils.bomb; @Component public class ConfigCipherUpdater { private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(ConfigCipherUpdater.class.getName()); private final SystemEnvironment systemEnvironment; private final TimeProvider timeProvider; protected static final String FLAWED_VALUE = "64d04c1676ce2085"; @Autowired public ConfigCipherUpdater(SystemEnvironment systemEnvironment, TimeProvider timeProvider) { this.systemEnvironment = systemEnvironment; this.timeProvider = timeProvider; } public void migrate() { File cipherFile = systemEnvironment.getCipherFile(); String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(timeProvider.currentTime()); File backupCipherFile = new File(systemEnvironment.getConfigDir(), "cipher.original." + timestamp); File configFile = new File(systemEnvironment.getCruiseConfigFile()); File backupConfigFile = new File(configFile.getParentFile(), configFile.getName() + ".original." + timestamp); try { if (!cipherFile.exists() || !FileUtils.readFileToString(cipherFile).equals(FLAWED_VALUE)) { return; } LOGGER.info("Found unsafe cipher {} on server, Go will make an attempt to rekey", FLAWED_VALUE); FileUtils.copyFile(cipherFile, backupCipherFile); LOGGER.info("Old cipher was successfully backed up to {}", backupCipherFile.getAbsoluteFile()); FileUtils.copyFile(configFile, backupConfigFile); LOGGER.info("Old config was successfully backed up to {}", backupConfigFile.getAbsoluteFile()); byte[] oldCipher = FileUtils.readFileToByteArray(backupCipherFile); new CipherProvider(systemEnvironment).resetCipher(); byte[] newCipher = FileUtils.readFileToByteArray(cipherFile); if (new String(newCipher).equals(new String(oldCipher))) { LOGGER.warn("Unable to generate a new safe cipher. Your cipher is unsafe."); FileUtils.deleteQuietly(backupCipherFile); FileUtils.deleteQuietly(backupConfigFile); return; } Document document = new SAXBuilder().build(configFile); List<String> encryptedAttributes = Arrays.asList("encryptedPassword", "encryptedManagerPassword"); List<String> encryptedNodes = Arrays.asList("encryptedValue"); XPathFactory xPathFactory = XPathFactory.instance(); for (String attributeName : encryptedAttributes) { XPathExpression<Element> xpathExpression = xPathFactory.compile(String.format("//*[@%s]", attributeName), Filters.element()); List<Element> encryptedPasswordElements = xpathExpression.evaluate(document); for (Element element : encryptedPasswordElements) { Attribute encryptedPassword = element.getAttribute(attributeName); encryptedPassword.setValue(reEncryptUsingNewKey(oldCipher, newCipher, encryptedPassword.getValue())); LOGGER.debug("Replaced encrypted value at {}", element.toString()); } } for (String nodeName : encryptedNodes) { XPathExpression<Element> xpathExpression = xPathFactory.compile(String.format("//%s", nodeName), Filters.element()); List<Element> encryptedNode = xpathExpression.evaluate(document); for (Element element : encryptedNode) { element.setText(reEncryptUsingNewKey(oldCipher, newCipher, element.getValue())); LOGGER.debug("Replaced encrypted value at {}", element.toString()); } } try (FileOutputStream fileOutputStream = new FileOutputStream(configFile)) { XmlUtils.writeXml(document, fileOutputStream); } LOGGER.info("Successfully re-encrypted config"); } catch (Exception e) { LOGGER.error("Re-keying of cipher failed with error: [{}]", e.getMessage(), e); if (backupCipherFile.exists()) { try { FileUtils.copyFile(backupCipherFile, cipherFile); } catch (IOException e1) { LOGGER.error("Could not replace the cipher file [{}] with original one [{}], please do so manually. Error: [{}]", cipherFile.getAbsolutePath(), backupCipherFile.getAbsolutePath(), e.getMessage(), e); bomb(e1); } } } } private String reEncryptUsingNewKey(byte[] oldCipher, byte[] newCipher, String encryptedValue) throws InvalidCipherTextException { GoCipher cipher = new GoCipher(); String decryptedValue = cipher.decipher(oldCipher, encryptedValue); return cipher.cipher(newCipher, decryptedValue); } }