utils,framework/db: Introduce new database encryption cipher based on AesGcmJce (#7003)

This commit is contained in:
Wei Zhou 2023-02-02 16:25:49 +01:00 committed by GitHub
parent 75a30058e9
commit 62e342c1bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1551 additions and 532 deletions

View File

@ -51,6 +51,7 @@ db.cloud.trustStorePassword=
# Encryption Settings
db.cloud.encryption.type=none
db.cloud.encrypt.secret=
db.cloud.encryptor.version=
# usage database settings
db.usage.username=@DBUSER@

1
debian/rules vendored
View File

@ -135,6 +135,7 @@ override_dh_auto_install:
install -D systemvm/dist/* $(DESTDIR)/usr/share/$(PACKAGE)-common/vms/
# We need jasypt for cloud-install-sys-tmplt, so this is a nasty hack to get it into the right place
install -D agent/target/dependencies/jasypt-1.9.3.jar $(DESTDIR)/usr/share/$(PACKAGE)-common/lib
install -D utils/target/cloud-utils-$(VERSION).jar $(DESTDIR)/usr/share/$(PACKAGE)-common/lib/$(PACKAGE)-utils.jar
# cloudstack-python
mkdir -p $(DESTDIR)/usr/share/pyshared

View File

@ -23,6 +23,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Date;
@ -109,6 +111,7 @@ import com.cloud.upgrade.dao.VersionDaoImpl;
import com.cloud.upgrade.dao.VersionVO;
import com.cloud.upgrade.dao.VersionVO.Step;
import com.cloud.utils.component.SystemIntegrityChecker;
import com.cloud.utils.crypt.DBEncryptionUtil;
import com.cloud.utils.db.GlobalLock;
import com.cloud.utils.db.ScriptRunner;
import com.cloud.utils.db.TransactionLegacy;
@ -369,6 +372,7 @@ public class DatabaseUpgradeChecker implements SystemIntegrityChecker {
}
try {
initializeDatabaseEncryptors();
final CloudStackVersion dbVersion = CloudStackVersion.parse(_dao.getCurrentVersion());
final String currentVersionValue = this.getClass().getPackage().getImplementationVersion();
@ -403,6 +407,40 @@ public class DatabaseUpgradeChecker implements SystemIntegrityChecker {
}
}
private void initializeDatabaseEncryptors() {
TransactionLegacy txn = TransactionLegacy.open("initializeDatabaseEncryptors");
txn.start();
String errorMessage = "Unable to get the database connections";
try {
Connection conn = txn.getConnection();
errorMessage = "Unable to get the 'init' value from 'configuration' table in the 'cloud' database";
decryptInit(conn);
txn.commit();
} catch (CloudRuntimeException e) {
s_logger.error(e.getMessage());
errorMessage = String.format("Unable to initialize the database encryptors due to %s. " +
"Please check if database encryption key and database encryptor version are correct.", errorMessage);
s_logger.error(errorMessage);
throw new CloudRuntimeException(errorMessage, e);
} catch (SQLException e) {
s_logger.error(errorMessage, e);
throw new CloudRuntimeException(errorMessage, e);
} finally {
txn.close();
}
}
private void decryptInit(Connection conn) throws SQLException {
String sql = "SELECT value from configuration WHERE name = 'init'";
try (PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet result = pstmt.executeQuery()) {
if (result.next()) {
String init = result.getString(1);
s_logger.info("init = " + DBEncryptionUtil.decrypt(init));
}
}
}
@VisibleForTesting
protected static final class NoopDbUpgrade implements DbUpgrade {

View File

@ -27,7 +27,6 @@ import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
import com.cloud.utils.crypt.DBEncryptionUtil;
import com.cloud.utils.exception.CloudRuntimeException;
@ -111,7 +110,7 @@ public class Upgrade450to451 implements DbUpgrade {
String preSharedKey = resultSet.getString(2);
try {
preSharedKey = DBEncryptionUtil.decrypt(preSharedKey);
} catch (EncryptionOperationNotPossibleException ignored) {
} catch (CloudRuntimeException ignored) {
s_logger.debug("The ipsec_psk preshared key id=" + rowId + "in remote_access_vpn is not encrypted, encrypting it.");
}
try (PreparedStatement updateStatement = conn.prepareStatement("UPDATE `cloud`.`remote_access_vpn` SET ipsec_psk=? WHERE id=?");) {

View File

@ -214,7 +214,7 @@ BEGIN
-- Add passphrase table
CREATE TABLE IF NOT EXISTS `cloud`.`passphrase` (
`id` bigint unsigned NOT NULL auto_increment,
`passphrase` varchar(64) DEFAULT NULL,
`passphrase` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@ -0,0 +1,30 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.cloud.utils.crypt;
import java.util.Map;
import java.util.Set;
public class DBEncryptionFinderCLI {
public static void main(String[] args) {
Map<String, Set<String>> encryptedTableCols = EncryptionSecretKeyChanger.findEncryptedTableColumns();
encryptedTableCols.forEach((table, cols) -> System.out.printf("Table %s has encrypted columns %s%n", table, cols));
}
}

View File

@ -22,149 +22,332 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
import org.jasypt.properties.EncryptableProperties;
import org.apache.commons.lang3.StringUtils;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.ReflectUtil;
import com.cloud.utils.db.Encrypt;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.exception.CloudRuntimeException;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import javax.persistence.Column;
import javax.persistence.Table;
/*
* EncryptionSecretKeyChanger updates Management Secret Key / DB Secret Key or both.
* DB secret key is validated against the key in db.properties
* db.properties is updated with values encrypted using new MS secret key
* server.properties is updated with values encrypted using new MS secret key
* DB data migrated using new DB secret key
*/
public class EncryptionSecretKeyChanger {
private StandardPBEStringEncryptor oldEncryptor = new StandardPBEStringEncryptor();
private StandardPBEStringEncryptor newEncryptor = new StandardPBEStringEncryptor();
private static final String keyFile = "/etc/cloudstack/management/key";
private CloudStackEncryptor oldEncryptor;
private CloudStackEncryptor newEncryptor;
private static final String KEY_FILE = "/etc/cloudstack/management/key";
private static final String ENV_NEW_MANAGEMENT_KEY = "CLOUD_SECRET_KEY_NEW";
private final Gson gson = new Gson();
private static final String PASSWORD = "password";
private static final Options options = initializeOptions();
private static final HelpFormatter helper = initializeHelper();
private static final String CMD_LINE_SYNTAX = "cloudstack-migrate-databases";
private static final int WIDTH = 100;
private static final String HEADER = "Options:";
private static final String FOOTER = " \nExamples: \n" +
" " + CMD_LINE_SYNTAX + " -m password -d password -n newmgmtkey -v V2 \n" +
" Migrate cloudstack properties (db.properties and server.properties) \n" +
" with new management key and encryptor V2. \n" +
" " + CMD_LINE_SYNTAX + " -m password -d password -n newmgmtkey -e newdbkey \n" +
" Migrate cloudstack properties and databases with new management key and database secret key. \n" +
" " + CMD_LINE_SYNTAX + " -m password -d password -n newmgmtkey -e newdbkey -s -v V2 \n" +
" Migrate cloudstack properties with new keys and encryptor V2, but skip database migration. \n" +
" " + CMD_LINE_SYNTAX + " -m password -d password -l -f \n" +
" Migrate cloudstack properties with new management key (load from $CLOUD_SECRET_KEY_NEW), \n" +
" and migrate database with old db key. \n" +
" \nReturn codes: \n" +
" 0 - Succeed to change keys and/or migrate databases \n" +
" 1 - Fail to parse the command line arguments \n" +
" 2 - Fail to validate parameters \n" +
" 3 - Fail to migrate database";
private static final String OLD_MS_KEY_OPTION = "oldMSKey";
private static final String OLD_DB_KEY_OPTION = "oldDBKey";
private static final String NEW_MS_KEY_OPTION = "newMSKey";
private static final String NEW_DB_KEY_OPTION = "newDBKey";
private static final String ENCRYPTOR_VERSION_OPTION = "version";
private static final String LOAD_NEW_MS_KEY_FROM_ENV_FLAG = "load-new-management-key-from-env";
private static final String FORCE_DATABASE_MIGRATION_FLAG = "force-database-migration";
private static final String SKIP_DATABASE_MIGRATION_FLAG = "skip-database-migration";
private static final String HELP_FLAG = "help";
public static void main(String[] args) {
List<String> argsList = Arrays.asList(args);
Iterator<String> iter = argsList.iterator();
String oldMSKey = null;
String oldDBKey = null;
String newMSKey = null;
String newDBKey = null;
if (args.length == 0 || StringUtils.equalsAny(args[0], "-h", "--help")) {
helper.printHelp(WIDTH, CMD_LINE_SYNTAX, HEADER, options, FOOTER, true);
System.exit(0);
}
//Parse command-line args
while (iter.hasNext()) {
String arg = iter.next();
// Old MS Key
if (arg.equals("-m")) {
oldMSKey = iter.next();
CommandLine cmdLine = null;
CommandLineParser parser = new DefaultParser();
try {
cmdLine = parser.parse(options, args);
} catch (ParseException e) {
System.out.println(e.getMessage());
helper.printHelp(WIDTH, CMD_LINE_SYNTAX, HEADER, options, FOOTER, true);
System.exit(1);
}
// Old DB Key
if (arg.equals("-d")) {
oldDBKey = iter.next();
String oldMSKey = cmdLine.getOptionValue(OLD_MS_KEY_OPTION);
String oldDBKey = cmdLine.getOptionValue(OLD_DB_KEY_OPTION);
String newMSKey = cmdLine.getOptionValue(NEW_MS_KEY_OPTION);
String newDBKey = cmdLine.getOptionValue(NEW_DB_KEY_OPTION);
String newEncryptorVersion = cmdLine.getOptionValue(ENCRYPTOR_VERSION_OPTION);
boolean loadNewMsKeyFromEnv = cmdLine.hasOption(LOAD_NEW_MS_KEY_FROM_ENV_FLAG);
boolean forced = cmdLine.hasOption(FORCE_DATABASE_MIGRATION_FLAG);
boolean skipped = cmdLine.hasOption(SKIP_DATABASE_MIGRATION_FLAG);
if (!validateParameters(oldMSKey, oldDBKey, newMSKey, newDBKey, newEncryptorVersion, loadNewMsKeyFromEnv)) {
helper.printHelp(WIDTH, CMD_LINE_SYNTAX, HEADER, options, FOOTER, true);
System.exit(2);
}
// New MS Key
if (arg.equals("-n")) {
newMSKey = iter.next();
System.out.println("Started database migration at " + new Date());
if (!migratePropertiesAndDatabase(oldMSKey, oldDBKey, newMSKey, newDBKey, newEncryptorVersion, loadNewMsKeyFromEnv, forced, skipped)) {
System.out.println("Got error during database migration at " + new Date());
System.exit(3);
}
// New DB Key
if (arg.equals("-e")) {
newDBKey = iter.next();
System.out.println("Finished database migration at " + new Date());
}
private static Options initializeOptions() {
Options options = new Options();
Option oldMSKey = Option.builder("m").longOpt(OLD_MS_KEY_OPTION).argName(OLD_MS_KEY_OPTION).required(true).hasArg().desc("(required) Current Mgmt Secret Key").build();
Option oldDBKey = Option.builder("d").longOpt(OLD_DB_KEY_OPTION).argName(OLD_DB_KEY_OPTION).required(true).hasArg().desc("(required) Current DB Secret Key").build();
Option newMSKey = Option.builder("n").longOpt(NEW_MS_KEY_OPTION).argName(NEW_MS_KEY_OPTION).required(false).hasArg().desc("New Mgmt Secret Key").build();
Option newDBKey = Option.builder("e").longOpt(NEW_DB_KEY_OPTION).argName(NEW_DB_KEY_OPTION).required(false).hasArg().desc("New DB Secret Key").build();
Option encryptorVersion = Option.builder("v").longOpt(ENCRYPTOR_VERSION_OPTION).argName(ENCRYPTOR_VERSION_OPTION).required(false).hasArg().desc("New DB Encryptor Version. Options are V1, V2.").build();
Option loadNewMsKeyFromEnv = Option.builder("l").longOpt(LOAD_NEW_MS_KEY_FROM_ENV_FLAG).desc("Load new management key from environment variable " + ENV_NEW_MANAGEMENT_KEY).build();
Option forceDatabaseMigration = Option.builder("f").longOpt(FORCE_DATABASE_MIGRATION_FLAG).desc("Force database migration even if DB Secret key is not changed").build();
Option skipDatabaseMigration = Option.builder("s").longOpt(SKIP_DATABASE_MIGRATION_FLAG).desc("Skip database migration even if DB Secret key is changed").build();
Option help = Option.builder("h").longOpt(HELP_FLAG).desc("Show help message").build();
options.addOption(oldMSKey);
options.addOption(oldDBKey);
options.addOption(newMSKey);
options.addOption(newDBKey);
options.addOption(encryptorVersion);
options.addOption(loadNewMsKeyFromEnv);
options.addOption(forceDatabaseMigration);
options.addOption(skipDatabaseMigration);
options.addOption(help);
return options;
}
private static HelpFormatter initializeHelper() {
HelpFormatter helper = new HelpFormatter();
helper.setOptionComparator((o1, o2) -> {
if (o1.isRequired() && !o2.isRequired()) {
return -1;
}
if (!o1.isRequired() && o2.isRequired()) {
return 1;
}
if (o1.hasArg() && !o2.hasArg()) {
return -1;
}
if (!o1.hasArg() && o2.hasArg()) {
return 1;
}
return o1.getOpt().compareTo(o2.getOpt());
});
return helper;
}
private static boolean validateParameters(String oldMSKey, String oldDBKey, String newMSKey, String newDBKey,
String newEncryptorVersion, boolean loadNewMsKeyFromEnv) {
if (oldMSKey == null || oldDBKey == null) {
System.out.println("Existing MS secret key or DB secret key is not provided");
usage();
return;
System.out.println("Existing Management secret key or DB secret key is not provided");
return false;
}
if (loadNewMsKeyFromEnv) {
if (StringUtils.isNotEmpty(newMSKey)) {
System.out.println("The new management key has already been set. Please check if it is set twice.");
return false;
}
newMSKey = System.getenv(ENV_NEW_MANAGEMENT_KEY);
if (StringUtils.isEmpty(newMSKey)) {
System.out.println("Environment variable " + ENV_NEW_MANAGEMENT_KEY + " is not set or empty");
return false;
}
}
if (newMSKey == null && newDBKey == null) {
System.out.println("New MS secret key and DB secret are both not provided");
usage();
return;
System.out.println("New Management secret key and DB secret are both not provided");
return false;
}
if (newEncryptorVersion != null) {
try {
CloudStackEncryptor.EncryptorVersion.fromString(newEncryptorVersion);
} catch (CloudRuntimeException ex) {
System.out.println(ex.getMessage());
return false;
}
}
return true;
}
private static boolean migratePropertiesAndDatabase(String oldMSKey, String oldDBKey, String newMSKey, String newDBKey,
String newEncryptorVersion, boolean loadNewMsKeyFromEnv,
boolean forced, boolean skipped) {
final File dbPropsFile = PropertiesUtil.findConfigFile("db.properties");
final Properties dbProps;
final Properties dbProps = new Properties();
EncryptionSecretKeyChanger keyChanger = new EncryptionSecretKeyChanger();
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
keyChanger.initEncryptor(encryptor, oldMSKey);
dbProps = new EncryptableProperties(encryptor);
PropertiesConfiguration backupDBProps = null;
System.out.println("Parsing db.properties file");
try(FileInputStream db_prop_fstream = new FileInputStream(dbPropsFile);) {
dbProps.load(db_prop_fstream);
try(FileInputStream dbPropFstream = new FileInputStream(dbPropsFile)) {
dbProps.load(dbPropFstream);
backupDBProps = new PropertiesConfiguration(dbPropsFile);
} catch (FileNotFoundException e) {
System.out.println("db.properties file not found while reading DB secret key" + e.getMessage());
System.out.println("db.properties file not found while reading DB secret key: " + e.getMessage());
return false;
} catch (IOException e) {
System.out.println("Error while reading DB secret key from db.properties" + e.getMessage());
System.out.println("Error while reading DB secret key from db.properties: " + e.getMessage());
return false;
} catch (ConfigurationException e) {
e.printStackTrace();
System.out.println("Error while getting configurations from db.properties: " + e.getMessage());
return false;
}
String dbSecretKey = null;
try {
dbSecretKey = dbProps.getProperty("db.cloud.encrypt.secret");
} catch (EncryptionOperationNotPossibleException e) {
System.out.println("Failed to decrypt existing DB secret key from db.properties. " + e.getMessage());
return;
EncryptionSecretKeyChecker.initEncryptor(oldMSKey);
EncryptionSecretKeyChecker.decryptAnyProperties(dbProps);
} catch (CloudRuntimeException e) {
System.out.println("Error: Incorrect Management Secret Key");
return false;
}
String dbSecretKey = dbProps.getProperty("db.cloud.encrypt.secret");
if (!oldDBKey.equals(dbSecretKey)) {
System.out.println("Incorrect MS Secret Key or DB Secret Key");
return;
System.out.println("Error: Incorrect DB Secret Key");
return false;
}
System.out.println("Secret key provided matched the key in db.properties");
System.out.println("DB Secret key provided matched the key in db.properties");
final String encryptionType = dbProps.getProperty("db.cloud.encryption.type");
final String oldEncryptorVersion = dbProps.getProperty("db.cloud.encryptor.version");
// validate old and new encryptor versions
try {
CloudStackEncryptor.EncryptorVersion.fromString(oldEncryptorVersion);
CloudStackEncryptor.EncryptorVersion.fromString(newEncryptorVersion);
} catch (CloudRuntimeException ex) {
System.out.println(ex.getMessage());
return false;
}
if (loadNewMsKeyFromEnv) {
newMSKey = System.getenv(ENV_NEW_MANAGEMENT_KEY);
}
if (newMSKey == null) {
System.out.println("No change in MS Key. Skipping migrating db.properties");
newMSKey = oldMSKey;
}
if (newDBKey == null) {
newDBKey = oldDBKey;
}
boolean isMSKeyChanged = !newMSKey.equals(oldMSKey);
boolean isDBKeyChanged = !newDBKey.equals(oldDBKey);
if (newEncryptorVersion == null && (isDBKeyChanged || forced) && !skipped) {
if (StringUtils.isNotEmpty(oldEncryptorVersion)) {
newEncryptorVersion = oldEncryptorVersion;
} else {
if (!keyChanger.migrateProperties(dbPropsFile, dbProps, newMSKey, newDBKey)) {
newEncryptorVersion = CloudStackEncryptor.EncryptorVersion.defaultVersion().name();
}
}
boolean isEncryptorVersionChanged = false;
if (newEncryptorVersion != null) {
isEncryptorVersionChanged = !newEncryptorVersion.equalsIgnoreCase(oldEncryptorVersion);
}
if (isMSKeyChanged || isDBKeyChanged || isEncryptorVersionChanged) {
System.out.println("INFO: Migrate properties with DB encryptor version: " + newEncryptorVersion);
if (!keyChanger.migrateProperties(dbPropsFile, dbProps, newMSKey, newDBKey, newEncryptorVersion)) {
System.out.println("Failed to update db.properties");
return;
} else {
return false;
}
if (!keyChanger.migrateServerProperties(newMSKey)) {
System.out.println("Failed to update server.properties");
return false;
}
//db.properties updated successfully
if (encryptionType.equals("file")) {
//update key file with new MS key
try (FileWriter fwriter = new FileWriter(keyFile);
BufferedWriter bwriter = new BufferedWriter(fwriter);)
try (FileWriter fwriter = new FileWriter(KEY_FILE);
BufferedWriter bwriter = new BufferedWriter(fwriter))
{
bwriter.write(newMSKey);
} catch (IOException e) {
System.out.println(String.format("Please update the file %s manually. Failed to write new secret to file with error %s", keyFile, e.getMessage()));
}
System.out.printf("Please update the file %s manually. Failed to write new secret to file with error %s %n", KEY_FILE, e.getMessage());
return false;
}
}
} else {
System.out.println("No changes with Management Secret Key, DB Secret Key and DB encryptor version. Skipping migrating db.properties");
}
boolean success = false;
if (newDBKey == null || newDBKey.equals(oldDBKey)) {
System.out.println("No change in DB Secret Key. Skipping Data Migration");
} else {
EncryptionSecretKeyChecker.initEncryptorForMigration(oldMSKey);
if (isDBKeyChanged || isEncryptorVersionChanged || forced) {
if (skipped) {
System.out.println("Skipping Data Migration as '-s' or '--skip-database-migration' is passed");
return true;
}
EncryptionSecretKeyChecker.initEncryptor(newMSKey);
try {
success = keyChanger.migrateData(oldDBKey, newDBKey);
success = keyChanger.migrateData(oldDBKey, newDBKey, oldEncryptorVersion, newEncryptorVersion);
} catch (Exception e) {
System.out.println("Error during data migration");
e.printStackTrace();
success = false;
}
} else {
System.out.println("No changes with DB Secret Key and DB encryptor version. Skipping Data Migration");
return true;
}
if (success) {
@ -179,36 +362,88 @@ public class EncryptionSecretKeyChanger {
}
if (encryptionType.equals("file")) {
//revert secret key in file
try (FileWriter fwriter = new FileWriter(keyFile);
BufferedWriter bwriter = new BufferedWriter(fwriter);)
try (FileWriter fwriter = new FileWriter(KEY_FILE);
BufferedWriter bwriter = new BufferedWriter(fwriter))
{
bwriter.write(oldMSKey);
} catch (IOException e) {
System.out.println("Failed to revert to old secret to file. Please update the file manually");
}
}
}
return false;
}
private boolean migrateProperties(File dbPropsFile, Properties dbProps, String newMSKey, String newDBKey) {
return true;
}
private boolean migrateServerProperties(String newMSKey) {
System.out.println("Migrating server.properties..");
final File serverPropsFile = PropertiesUtil.findConfigFile("server.properties");
final Properties serverProps = new Properties();
PropertiesConfiguration newServerProps;
try(FileInputStream serverPropFstream = new FileInputStream(serverPropsFile)) {
serverProps.load(serverPropFstream);
newServerProps = new PropertiesConfiguration(serverPropsFile);
} catch (FileNotFoundException e) {
System.out.println("server.properties file not found: " + e.getMessage());
return false;
} catch (IOException e) {
System.out.println("Error while reading server.properties: " + e.getMessage());
return false;
} catch (ConfigurationException e) {
System.out.println("Error while getting configurations from server.properties: " + e.getMessage());
return false;
}
try {
EncryptionSecretKeyChecker.decryptAnyProperties(serverProps);
} catch (CloudRuntimeException e) {
System.out.println(e.getMessage());
return false;
}
CloudStackEncryptor msEncryptor = new CloudStackEncryptor(newMSKey, null, getClass());
try {
String encryptionType = serverProps.getProperty("password.encryption.type");
if (StringUtils.isEmpty(encryptionType) || encryptionType.equalsIgnoreCase("none")) {
System.out.println("Skipping server.properties as password.encryption.type is " + encryptionType);
return true;
}
String keystorePassword = serverProps.getProperty("https.keystore.password");
if (StringUtils.isNotEmpty(keystorePassword)) {
newServerProps.setProperty("https.keystore.password", "ENC(" + msEncryptor.encrypt(keystorePassword) + ")");
}
newServerProps.save(serverPropsFile.getAbsolutePath());
} catch (Exception e) {
System.out.println(e.getMessage());
return false;
}
System.out.println("Migrating server.properties Done.");
return true;
}
private boolean migrateProperties(File dbPropsFile, Properties dbProps, String newMSKey, String newDBKey, String newEncryptorVersion) {
System.out.println("Migrating db.properties..");
StandardPBEStringEncryptor msEncryptor = new StandardPBEStringEncryptor();
;
initEncryptor(msEncryptor, newMSKey);
CloudStackEncryptor msEncryptor = new CloudStackEncryptor(newMSKey, null, getClass());
try {
PropertiesConfiguration newDBProps = new PropertiesConfiguration(dbPropsFile);
if (newDBKey != null && !newDBKey.isEmpty()) {
if (StringUtils.isNotEmpty(newDBKey)) {
newDBProps.setProperty("db.cloud.encrypt.secret", "ENC(" + msEncryptor.encrypt(newDBKey) + ")");
}
String prop = dbProps.getProperty("db.cloud.password");
if (prop != null && !prop.isEmpty()) {
if (StringUtils.isNotEmpty(prop)) {
newDBProps.setProperty("db.cloud.password", "ENC(" + msEncryptor.encrypt(prop) + ")");
}
prop = dbProps.getProperty("db.usage.password");
if (prop != null && !prop.isEmpty()) {
if (StringUtils.isNotEmpty(prop)) {
newDBProps.setProperty("db.usage.password", "ENC(" + msEncryptor.encrypt(prop) + ")");
}
if (newEncryptorVersion != null) {
newDBProps.setProperty("db.cloud.encryptor.version", newEncryptorVersion);
}
newDBProps.save(dbPropsFile.getAbsolutePath());
} catch (Exception e) {
e.printStackTrace();
@ -218,10 +453,10 @@ public class EncryptionSecretKeyChanger {
return true;
}
private boolean migrateData(String oldDBKey, String newDBKey) {
private boolean migrateData(String oldDBKey, String newDBKey, String oldEncryptorVersion, String newEncryptorVersion) throws SQLException {
System.out.println("Begin Data migration");
initEncryptor(oldEncryptor, oldDBKey);
initEncryptor(newEncryptor, newDBKey);
oldEncryptor = new CloudStackEncryptor(oldDBKey, oldEncryptorVersion, getClass());
newEncryptor = new CloudStackEncryptor(newDBKey, newEncryptorVersion, getClass());
System.out.println("Initialised Encryptors");
TransactionLegacy txn = TransactionLegacy.open("Migrate");
@ -231,13 +466,28 @@ public class EncryptionSecretKeyChanger {
try {
conn = txn.getConnection();
} catch (SQLException e) {
System.out.println("Unable to migrate encrypted data in the database due to: " + e.getMessage());
throw new CloudRuntimeException("Unable to migrate encrypted data in the database", e);
}
// migrate values in configuration
migrateConfigValues(conn);
// migrate resource details values
migrateHostDetails(conn);
migrateVNCPassword(conn);
migrateUserCredentials(conn);
migrateClusterDetails(conn);
migrateImageStoreDetails(conn);
migrateStoragePoolDetails(conn);
migrateScaleIOStoragePoolDetails(conn);
migrateUserVmDetails(conn);
// migrate other encrypted fields
migrateTemplateDeployAsIsDetails(conn);
migrateImageStoreUrlForCifs(conn);
migrateStoragePoolPathForSMB(conn);
// migrate columns with annotation @Encrypt
migrateEncryptedTableColumns(conn);
txn.commit();
} finally {
@ -247,125 +497,300 @@ public class EncryptionSecretKeyChanger {
return true;
}
private void initEncryptor(StandardPBEStringEncryptor encryptor, String secretKey) {
encryptor.setAlgorithm("PBEWithMD5AndDES");
SimpleStringPBEConfig stringConfig = new SimpleStringPBEConfig();
stringConfig.setPassword(secretKey);
encryptor.setConfig(stringConfig);
}
private String migrateValue(String value) {
if (value == null || value.isEmpty()) {
protected String migrateValue(String value) {
if (StringUtils.isEmpty(value)) {
return value;
}
String decryptVal = oldEncryptor.decrypt(value);
return newEncryptor.encrypt(decryptVal);
}
protected String migrateUrlOrPath(String urlOrPath) {
if (StringUtils.isEmpty(urlOrPath)) {
return urlOrPath;
}
String[] properties = urlOrPath.split("&");
for (String property : properties) {
if (property.startsWith("password=")) {
String password = property.substring(property.indexOf("=") + 1);
password = migrateValue(password);
return urlOrPath.replaceAll(property, "password=" + password);
}
}
return urlOrPath;
}
private void migrateConfigValues(Connection conn) {
System.out.println("Begin migrate config values");
try(PreparedStatement select_pstmt = conn.prepareStatement("select name, value from configuration where category in ('Hidden', 'Secure')");
ResultSet rs = select_pstmt.executeQuery();
PreparedStatement update_pstmt = conn.prepareStatement("update configuration set value=? where name=?");
String tableName = "configuration";
String selectSql = "SELECT name, value FROM configuration WHERE category IN ('Hidden', 'Secure')";
String updateSql = "UPDATE configuration SET value=? WHERE name=?";
migrateValueAndUpdateDatabaseByName(conn, tableName, selectSql, updateSql);
System.out.println("End migrate config values");
}
private void migrateValueAndUpdateDatabaseById(Connection conn, String tableName, String selectSql, String updateSql, boolean isUrlOrPath) {
try( PreparedStatement selectPstmt = conn.prepareStatement(selectSql);
ResultSet rs = selectPstmt.executeQuery();
PreparedStatement updatePstmt = conn.prepareStatement(updateSql)
) {
while (rs.next()) {
long id = rs.getLong(1);
String value = rs.getString(2);
if (StringUtils.isEmpty(value)) {
continue;
}
String encryptedValue = isUrlOrPath ? migrateUrlOrPath(value) : migrateValue(value);
updatePstmt.setBytes(1, encryptedValue.getBytes(StandardCharsets.UTF_8));
updatePstmt.setLong(2, id);
updatePstmt.executeUpdate();
}
} catch (SQLException e) {
throwCloudRuntimeException(String.format("Unable to update %s values", tableName), e);
}
}
private void migrateValueAndUpdateDatabaseByName(Connection conn, String tableName, String selectSql, String updateSql) {
try(PreparedStatement selectPstmt = conn.prepareStatement(selectSql);
ResultSet rs = selectPstmt.executeQuery();
PreparedStatement updatePstmt = conn.prepareStatement(updateSql)
) {
while (rs.next()) {
String name = rs.getString(1);
String value = rs.getString(2);
if (value == null || value.isEmpty()) {
if (StringUtils.isEmpty(value)) {
continue;
}
String encryptedValue = migrateValue(value);
update_pstmt.setBytes(1, encryptedValue.getBytes("UTF-8"));
update_pstmt.setString(2, name);
update_pstmt.executeUpdate();
updatePstmt.setBytes(1, encryptedValue.getBytes(StandardCharsets.UTF_8));
updatePstmt.setString(2, name);
updatePstmt.executeUpdate();
}
} catch (SQLException e) {
throw new CloudRuntimeException("Unable to update configuration values ", e);
} catch (UnsupportedEncodingException e) {
throw new CloudRuntimeException("Unable to update configuration values ", e);
throwCloudRuntimeException(String.format("Unable to update %s values", tableName), e);
}
System.out.println("End migrate config values");
}
private void migrateHostDetails(Connection conn) {
System.out.println("Begin migrate host details");
try( PreparedStatement sel_pstmt = conn.prepareStatement("select id, value from host_details where name = 'password'");
ResultSet rs = sel_pstmt.executeQuery();
PreparedStatement pstmt = conn.prepareStatement("update host_details set value=? where id=?");
) {
while (rs.next()) {
long id = rs.getLong(1);
String value = rs.getString(2);
if (value == null || value.isEmpty()) {
continue;
}
String encryptedValue = migrateValue(value);
pstmt.setBytes(1, encryptedValue.getBytes("UTF-8"));
pstmt.setLong(2, id);
pstmt.executeUpdate();
}
} catch (SQLException e) {
throw new CloudRuntimeException("Unable update host_details values ", e);
} catch (UnsupportedEncodingException e) {
throw new CloudRuntimeException("Unable update host_details values ", e);
}
migrateDetails(conn, "host_details", PASSWORD);
System.out.println("End migrate host details");
}
private void migrateVNCPassword(Connection conn) {
System.out.println("Begin migrate VNC password");
try(PreparedStatement select_pstmt = conn.prepareStatement("select id, vnc_password from vm_instance");
ResultSet rs = select_pstmt.executeQuery();
PreparedStatement pstmt = conn.prepareStatement("update vm_instance set vnc_password=? where id=?");
private void migrateClusterDetails(Connection conn) {
System.out.println("Begin migrate cluster details");
migrateDetails(conn, "cluster_details", PASSWORD);
System.out.println("End migrate cluster details");
}
private void migrateImageStoreDetails(Connection conn) {
System.out.println("Begin migrate image store details");
migrateDetails(conn, "image_store_details", "key", "secretkey");
System.out.println("End migrate image store details");
}
private void migrateStoragePoolDetails(Connection conn) {
System.out.println("Begin migrate storage pool details");
migrateDetails(conn, "storage_pool_details", PASSWORD);
System.out.println("End migrate storage pool details");
}
private void migrateScaleIOStoragePoolDetails(Connection conn) {
System.out.println("Begin migrate storage pool details for ScaleIO");
migrateDetails(conn, "storage_pool_details", "powerflex.gw.username", "powerflex.gw.password");
System.out.println("End migrate storage pool details for ScaleIO");
}
private void migrateUserVmDetails(Connection conn) {
System.out.println("Begin migrate user vm details");
migrateDetails(conn, "user_vm_details", PASSWORD);
System.out.println("End migrate user vm details");
}
private void migrateDetails(Connection conn, String tableName, String... detailNames) {
String convertedDetails = Arrays.stream(detailNames).map(detail -> "'" + detail + "'").collect(Collectors.joining(", "));
String selectSql = String.format("SELECT id, value FROM %s WHERE name IN (%s)", tableName, convertedDetails);
String updateSql = String.format("UPDATE %s SET value=? WHERE id=?", tableName);
migrateValueAndUpdateDatabaseById(conn, tableName, selectSql, updateSql, false);
}
private void migrateTemplateDeployAsIsDetails(Connection conn) throws SQLException {
System.out.println("Begin migrate user vm deploy_as_is details");
if (!ifTableExists(conn.getMetaData(), "user_vm_deploy_as_is_details")) {
System.out.printf("Skipped as table %s does not exist %n", "user_vm_deploy_as_is_details");
return;
}
if (!ifTableExists(conn.getMetaData(), "template_deploy_as_is_details")) {
System.out.printf("Skipped as table %s does not exist %n", "template_deploy_as_is_details");
return;
}
String sqlTemplateDeployAsIsDetails = "SELECT template_deploy_as_is_details.value " +
"FROM template_deploy_as_is_details JOIN vm_instance " +
"WHERE template_deploy_as_is_details.template_id = vm_instance.vm_template_id " +
"vm_instance.id = %s AND template_deploy_as_is_details.name = '%s' LIMIT 1";
try (PreparedStatement selectPstmt = conn.prepareStatement("SELECT id, vm_id, name, value FROM user_vm_deploy_as_is_details");
ResultSet rs = selectPstmt.executeQuery();
PreparedStatement updatePstmt = conn.prepareStatement("UPDATE user_vm_deploy_as_is_details SET value=? WHERE id=?")
) {
while (rs.next()) {
long id = rs.getLong(1);
String value = rs.getString(2);
if (value == null || value.isEmpty()) {
long vmId = rs.getLong(2);
String name = rs.getString(3);
String value = rs.getString(4);
if (StringUtils.isEmpty(value)) {
continue;
}
String key = name.startsWith("property-") ? name : "property-" + name;
try (PreparedStatement pstmtTemplateDeployAsIs = conn.prepareStatement(String.format(sqlTemplateDeployAsIsDetails, vmId, key));
ResultSet rsTemplateDeployAsIs = pstmtTemplateDeployAsIs.executeQuery()) {
if (rsTemplateDeployAsIs.next()) {
String templateDeployAsIsDetailValue = rsTemplateDeployAsIs.getString(1);
OVFPropertyTO property = gson.fromJson(templateDeployAsIsDetailValue, OVFPropertyTO.class);
if (property != null && property.isPassword()) {
String encryptedValue = migrateValue(value);
updatePstmt.setBytes(1, encryptedValue.getBytes(StandardCharsets.UTF_8));
updatePstmt.setLong(2, id);
updatePstmt.executeUpdate();
}
}
}
}
} catch (SQLException | JsonSyntaxException e) {
throwCloudRuntimeException("Unable to update user_vm_deploy_as_is_details values", e);
}
System.out.println("End migrate user vm deploy_as_is details");
}
pstmt.setBytes(1, encryptedValue.getBytes("UTF-8"));
pstmt.setLong(2, id);
pstmt.executeUpdate();
private void migrateImageStoreUrlForCifs(Connection conn) {
System.out.println("Begin migrate image store url if protocol is cifs");
String tableName = "image_store";
String fieldName = "url";
if (getCountOfTable(conn, tableName) == 0) {
System.out.printf("Skipped table %s as there is no image store with protocol is cifs in the table %n", tableName);
return;
}
String selectSql = String.format("SELECT id, `%s` FROM %s WHERE protocol = 'cifs'", fieldName, tableName);
String updateSql = String.format("UPDATE %s SET `%s`=? WHERE id=?", tableName, fieldName);
migrateValueAndUpdateDatabaseById(conn, tableName, selectSql, updateSql, true);
System.out.println("End migrate image store url if protocol is cifs");
}
private void migrateStoragePoolPathForSMB(Connection conn) {
System.out.println("Begin migrate storage pool path if pool type is SMB");
String tableName = "storage_pool";
String fieldName = "path";
if (getCountOfTable(conn, tableName) == 0) {
System.out.printf("Skipped table %s as there is no storage pool with pool type is SMB in the table %n", tableName);
return;
}
String selectSql = String.format("SELECT id, `%s` FROM %s WHERE pool_type = 'SMB'", fieldName, tableName);
String updateSql = String.format("UPDATE %s SET `%s`=? WHERE id=?", tableName, fieldName);
migrateValueAndUpdateDatabaseById(conn, tableName, selectSql, updateSql, true);
System.out.println("End migrate storage pool path if pool type is SMB");
}
private void migrateDatabaseField(Connection conn, String tableName, String fieldName) {
System.out.printf("Begin migrate table %s field %s %n", tableName, fieldName);
String selectSql = String.format("SELECT id, `%s` FROM %s", fieldName, tableName);
String updateSql = String.format("UPDATE %s SET `%s`=? WHERE id=?", tableName, fieldName);
migrateValueAndUpdateDatabaseById(conn, tableName, selectSql, updateSql, false);
System.out.printf("Done migrating database field %s.%s %n", tableName, fieldName);
}
protected static Map<String, Set<String>> findEncryptedTableColumns() {
Map<String, Set<String>> tableCols = new HashMap<>();
Set<Class<?>> vos = ReflectUtil.getClassesWithAnnotation(Table.class, new String[]{"com", "org"});
vos.forEach( vo -> {
Table tableAnnotation = vo.getAnnotation(Table.class);
if (tableAnnotation == null || (tableAnnotation.name() != null && tableAnnotation.name().endsWith("_view"))) {
return;
}
for (Field field : vo.getDeclaredFields()) {
if (field.isAnnotationPresent(Encrypt.class)) {
Set<String> encryptedColumns = tableCols.getOrDefault(tableAnnotation.name(), new HashSet<>());
String columnName = field.getName();
if (field.isAnnotationPresent(Column.class)) {
Column columnAnnotation = field.getAnnotation(Column.class);
columnName = columnAnnotation.name();
}
encryptedColumns.add(columnName);
tableCols.put(tableAnnotation.name(), encryptedColumns);
}
}
});
return tableCols;
}
private void migrateEncryptedTableColumns(Connection conn) throws SQLException {
Map<String, Set<String>> encryptedTableCols = findEncryptedTableColumns();
DatabaseMetaData metadata = conn.getMetaData();
encryptedTableCols.forEach((table, columns) -> {
if (!ifTableExists(metadata, table)) {
System.out.printf("Skipped table %s as it does not exist %n", table);
return;
}
if (getCountOfTable(conn, table) == 0) {
System.out.printf("Skipped table %s as there is no data in the table %n", table);
return;
}
columns.forEach(column -> {
if (!ifTableColumnExists(metadata, table, column)) {
System.out.printf("Skipped column %s in table %s as it does not exist %n", column, table);
return;
}
migrateDatabaseField(conn, table, column);
});
});
}
private boolean ifTableExists(DatabaseMetaData metadata, String table) {
try {
ResultSet rs = metadata.getTables(null, null, table, null);
if (rs.next()) {
return true;
}
} catch (SQLException e) {
throw new CloudRuntimeException("Unable update vm_instance vnc_password ", e);
} catch (UnsupportedEncodingException e) {
throw new CloudRuntimeException("Unable update vm_instance vnc_password ", e);
throwCloudRuntimeException(String.format("Unable to get table %s", table), e);
}
System.out.println("End migrate VNC password");
return false;
}
private void migrateUserCredentials(Connection conn) {
System.out.println("Begin migrate user credentials");
try(PreparedStatement select_pstmt = conn.prepareStatement("select id, secret_key from user");
ResultSet rs = select_pstmt.executeQuery();
PreparedStatement pstmt = conn.prepareStatement("update user set secret_key=? where id=?");
) {
while (rs.next()) {
long id = rs.getLong(1);
String secretKey = rs.getString(2);
if (secretKey == null || secretKey.isEmpty()) {
continue;
}
String encryptedSecretKey = migrateValue(secretKey);
pstmt.setBytes(1, encryptedSecretKey.getBytes("UTF-8"));
pstmt.setLong(2, id);
pstmt.executeUpdate();
private boolean ifTableColumnExists(DatabaseMetaData metadata, String table, String column) {
try {
ResultSet rs = metadata.getColumns(null, null, table, column);
if (rs.next()) {
return true;
}
} catch (SQLException e) {
throw new CloudRuntimeException("Unable update user secret key ", e);
} catch (UnsupportedEncodingException e) {
throw new CloudRuntimeException("Unable update user secret key ", e);
throwCloudRuntimeException(String.format("Unable to get column %s in table %s", column, table), e);
}
System.out.println("End migrate user credentials");
return false;
}
private static void usage() {
System.out.println("Usage: \tEncryptionSecretKeyChanger \n" + "\t\t-m <Mgmt Secret Key> \n" + "\t\t-d <DB Secret Key> \n" + "\t\t-n [New Mgmt Secret Key] \n"
+ "\t\t-e [New DB Secret Key]");
private int getCountOfTable(Connection conn, String table) {
try (PreparedStatement pstmt = conn.prepareStatement(String.format("SELECT count(*) FROM %s", table));
ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
} catch (SQLException e) {
throwCloudRuntimeException(String.format("Unable to get count of records in table %s", table), e);
}
return 0;
}
private static void throwCloudRuntimeException(String msg, Exception e) {
System.out.println(msg + " due to: " + e.getMessage());
throw new CloudRuntimeException(msg, e);
}
}

View File

@ -0,0 +1,130 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.cloud.utils.crypt;
/**
* This is a copy of ./api/src/main/java/com/cloud/agent/api/to/deployasis/OVFPropertyTO.java
*/
public class OVFPropertyTO {
private String key;
private String type;
private String value;
private String qualifiers;
private Boolean userConfigurable;
private String label;
private String description;
private Boolean password;
private int index;
private String category;
public OVFPropertyTO() {
}
public OVFPropertyTO(String key, String type, String value, String qualifiers, boolean userConfigurable,
String label, String description, boolean password, int index, String category) {
this.key = key;
this.type = type;
this.value = value;
this.qualifiers = qualifiers;
this.userConfigurable = userConfigurable;
this.label = label;
this.description = description;
this.password = password;
this.index = index;
this.category = category;
}
public Long getTemplateId() {
return null;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getQualifiers() {
return qualifiers;
}
public void setQualifiers(String qualifiers) {
this.qualifiers = qualifiers;
}
public Boolean isUserConfigurable() {
return userConfigurable;
}
public void setUserConfigurable(Boolean userConfigurable) {
this.userConfigurable = userConfigurable;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Boolean isPassword() {
return password;
}
public void setPassword(Boolean password) {
this.password = password;
}
public String getCategory() {
return category;
}
public int getIndex() {
return index;
}
}

View File

@ -0,0 +1,87 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.cloud.utils.crypt;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.springframework.test.util.ReflectionTestUtils;
public class EncryptionSecretKeyChangerTest {
@Spy
EncryptionSecretKeyChanger changer = new EncryptionSecretKeyChanger();
@Mock
CloudStackEncryptor oldEncryptor;
@Mock
CloudStackEncryptor newEncryptor;
private static final String emtpyString = "";
private static final String encryptedValue = "encryptedValue";
private static final String plainText = "plaintext";
private static final String newEncryptedValue = "newEncryptedValue";
@Before
public void setUp() {
oldEncryptor = Mockito.mock(CloudStackEncryptor.class);
newEncryptor = Mockito.mock(CloudStackEncryptor.class);
ReflectionTestUtils.setField(changer, "oldEncryptor", oldEncryptor);
ReflectionTestUtils.setField(changer, "newEncryptor", newEncryptor);
Mockito.when(oldEncryptor.decrypt(encryptedValue)).thenReturn(plainText);
Mockito.when(newEncryptor.encrypt(plainText)).thenReturn(newEncryptedValue);
}
@Test
public void migrateValueTest() {
String value = changer.migrateValue(encryptedValue);
Assert.assertEquals(newEncryptedValue, value);
Mockito.verify(oldEncryptor).decrypt(encryptedValue);
Mockito.verify(newEncryptor).encrypt(plainText);
}
@Test
public void migrateValueTest2() {
String value = changer.migrateValue(emtpyString);
Assert.assertEquals(emtpyString, value);
}
@Test
public void migrateUrlOrPathTest() {
String path = emtpyString;
Assert.assertEquals(path, changer.migrateUrlOrPath(path));
path = String.format("password=%s", encryptedValue);
Assert.assertEquals(path.replaceAll("password=" + encryptedValue, "password=" + newEncryptedValue), changer.migrateUrlOrPath(path));
path = String.format("username=user&password=%s", encryptedValue);
Assert.assertEquals(path.replaceAll("password=" + encryptedValue, "password=" + newEncryptedValue), changer.migrateUrlOrPath(path));
path = String.format("username=user&password2=%s", encryptedValue);
Assert.assertEquals(path, changer.migrateUrlOrPath(path));
path = String.format("username=user&password=%s&add=false", encryptedValue);
Assert.assertEquals(path.replaceAll("password=" + encryptedValue, "password=" + newEncryptedValue), changer.migrateUrlOrPath(path));
}
}

View File

@ -299,6 +299,7 @@ ln -sf log4j-cloud.xml ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/management/log4j
install python/bindir/cloud-external-ipallocator.py ${RPM_BUILD_ROOT}%{_bindir}/%{name}-external-ipallocator.py
install -D client/target/pythonlibs/jasypt-1.9.3.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/lib/jasypt-1.9.3.jar
install -D utils/target/cloud-utils-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/lib/%{name}-utils.jar
install -D packaging/centos7/cloud-ipallocator.rc ${RPM_BUILD_ROOT}%{_initrddir}/%{name}-ipallocator
install -D packaging/centos7/cloud.limits ${RPM_BUILD_ROOT}%{_sysconfdir}/security/limits.d/cloud
@ -648,6 +649,7 @@ pip3 install --upgrade urllib3
%attr(0644,root,root) %{python_sitearch}/__pycache__/*
%attr(0644,root,root) %{python_sitearch}/cloudutils/*
%attr(0644, root, root) %{_datadir}/%{name}-common/lib/jasypt-1.9.3.jar
%attr(0644, root, root) %{_datadir}/%{name}-common/lib/%{name}-utils.jar
%{_defaultdocdir}/%{name}-common-%{version}/LICENSE
%{_defaultdocdir}/%{name}-common-%{version}/NOTICE

View File

@ -281,6 +281,7 @@ ln -sf log4j-cloud.xml ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/management/log4j
install python/bindir/cloud-external-ipallocator.py ${RPM_BUILD_ROOT}%{_bindir}/%{name}-external-ipallocator.py
install -D client/target/pythonlibs/jasypt-1.9.3.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/lib/jasypt-1.9.3.jar
install -D utils/target/cloud-utils-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/lib/%{name}-utils.jar
install -D packaging/centos8/cloud-ipallocator.rc ${RPM_BUILD_ROOT}%{_initrddir}/%{name}-ipallocator
install -D packaging/centos8/cloud.limits ${RPM_BUILD_ROOT}%{_sysconfdir}/security/limits.d/cloud
@ -626,6 +627,7 @@ pip install --upgrade /usr/share/cloudstack-marvin/Marvin-*.tar.gz
%attr(0644,root,root) %{_datadir}/%{name}-common/python-site/__pycache__/*
%attr(0644,root,root) %{_datadir}/%{name}-common/python-site/cloudutils/*
%attr(0644, root, root) %{_datadir}/%{name}-common/lib/jasypt-1.9.3.jar
%attr(0644, root, root) %{_datadir}/%{name}-common/lib/%{name}-utils.jar
%{_defaultdocdir}/%{name}-common-%{version}/LICENSE
%{_defaultdocdir}/%{name}-common-%{version}/NOTICE

View File

@ -86,6 +86,7 @@
<!-- Apache Commons versions -->
<cs.codec.version>1.15</cs.codec.version>
<cs.commons-cli.version>1.5.0</cs.commons-cli.version>
<cs.commons-collections.version>4.4</cs.commons-collections.version>
<cs.commons-compress.version>1.21</cs.commons-compress.version>
<cs.commons-exec.version>1.3</cs.commons-exec.version>
@ -171,6 +172,7 @@
<cs.reflections.version>0.9.12</cs.reflections.version>
<cs.servicemix.version>3.4.4_1</cs.servicemix.version>
<cs.servlet.version>4.0.1</cs.servlet.version>
<cs.tink.version>1.7.0</cs.tink.version>
<cs.tomcat-embed-core.version>10.0.22</cs.tomcat-embed-core.version>
<cs.trilead.version>build-217-jenkins-27</cs.trilead.version>
<cs.vmware.api.version>8.0</cs.vmware.api.version>

View File

@ -55,7 +55,7 @@ dbHost="localhost"
dbUser="root"
dbPassword=
dbPort=3306
jasypt='/usr/share/cloudstack-common/lib/jasypt-1.9.3.jar'
jarfile='/usr/share/cloudstack-common/lib/cloudstack-utils.jar'
# check if first parameter is not a dash (-) then print the usage block
if [[ ! $@ =~ ^\-.+ ]]; then
@ -149,7 +149,7 @@ if [[ -f /etc/cloudstack/management/db.properties ]]; then
if [[ "$encType" == "file" || "$encType" == "web" ]]; then
encPassword=$(sed '/^\#/d' /etc/cloudstack/management/db.properties | grep 'db.cloud.password' | tail -n 1 | cut -d "=" -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'i | sed 's/^ENC(\(.*\))/\1/')
if [[ ! $encPassword == "" ]]; then
dbPassword=(`java -classpath $jasypt org.jasypt.intf.cli.JasyptPBEStringDecryptionCLI decrypt.sh input=$encPassword password=$msKey verbose=false`)
dbPassword=(`java -classpath $jarfile com.cloud.utils.crypt.EncryptionCLI -d -i "$encPassword" -p "$msKey"`)
if [[ ! $dbPassword ]]; then
failed 2 "Failed to decrypt DB password from db.properties"
fi

View File

@ -162,8 +162,9 @@ public class ConfigurationServerImpl extends ManagerBase implements Configuratio
try {
persistDefaultValues();
_configDepotAdmin.populateConfigurations();
} catch (InternalErrorException e) {
throw new RuntimeException("Unhandled configuration exception", e);
} catch (InternalErrorException | CloudRuntimeException e) {
s_logger.error("Unhandled configuration exception: " + e.getMessage());
throw new CloudRuntimeException("Unhandled configuration exception", e);
}
return true;
}

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python3
#!/bin/bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
@ -17,271 +17,32 @@
# specific language governing permissions and limitations
# under the License.
LOGFILE=/tmp/cloudstack-migrate-databases.log
import os,logging,sys
from optparse import OptionParser
import mysql.connector
import subprocess
import glob
check_if_svc_active() {
svc_name=$1
systemctl is-active $svc_name -q
if [ $? -eq 0 ];then
echo "service $svc_name is still active. Please stop it and retry." |tee -a ${LOGFILE}
exit 1
fi
}
# ---- This snippet of code adds the sources path and the waf configured PYTHONDIR to the Python path ----
# ---- We do this so cloud_utils can be looked up in the following order:
# ---- 1) Sources directory
# ---- 2) waf configured PYTHONDIR
# ---- 3) System Python path
for pythonpath in (
"@PYTHONDIR@",
os.path.join(os.path.dirname(__file__),os.path.pardir,os.path.pardir,"python","lib"),
):
if os.path.isdir(pythonpath): sys.path.insert(0,pythonpath)
# ---- End snippet of code ----
from cloud_utils import check_selinux, CheckFailed, resolves_to_ipv6
import cloud_utils
if [ "$1" != "" ] && [ "$1" != "-h" ] && [ "$1" != "--help" ];then
check_if_svc_active "cloudstack-management"
check_if_svc_active "cloudstack-usage"
fi
# RUN ME LIKE THIS
# python setup/bindir/cloud-migrate-databases.in --config=client/conf/override/db.properties --resourcedir=setup/db --dry-run
# --dry-run makes it so the changes to the database in the context of the migrator are rolled back
java -classpath /etc/cloudstack/management:/usr/share/cloudstack-management/lib/* \
com.cloud.utils.crypt.EncryptionSecretKeyChanger \
"$@" \
> >(tee -a ${LOGFILE}) 2> >(tee -a ${LOGFILE} >/dev/null)
# This program / library breaks down as follows:
# high-level breakdown:
# the module calls main()
# main processes command-line options
# main() instantiates a migrator with a a list of possible migration steps
# migrator discovers and topologically sorts migration steps from the given list
# main() run()s the migrator
# for each one of the migration steps:
# the migrator instantiates the migration step with the context as first parameter
# the instantiated migration step saves the context onto itself as self.context
# the migrator run()s the instantiated migration step. within run(), self.context is the context
# the migrator commits the migration context to the database (or rollsback if --dry-run is specified)
# that is it
res=$?
if [ $res -eq 0 ];then
rm -f $LOGFILE
else
echo "Failed to migrate databases. You may find more logs in $LOGFILE"
fi
# The specific library code is in cloud_utils.py
# What needs to be implemented is MigrationSteps
# Specifically in the FromInitialTo21 evolver.
# What Db20to21MigrationUtil.java does, needs to be done within run() of that class
# refer to the class docstring to find out how
# implement them below
class CloudContext(cloud_utils.MigrationContext):
def __init__(self,host,port,username,password,database,configdir,resourcedir):
self.host = host
self.port = port
self.username = username
self.password = password
self.database = database
self.configdir = configdir
self.resourcedir = resourcedir
self.conn = mysql.connector.connect(host=self.host,
user=self.username,
password=self.password,
database=self.database,
port=self.port)
self.conn.autocommit(False)
self.db = self.conn.cursor()
def wrapex(func):
sqlogger = logging.getLogger("SQL")
def f(stmt,parms=None):
if parms: sqlogger.debug("%s | with parms %s",stmt,parms)
else: sqlogger.debug("%s",stmt)
return func(stmt,parms)
return f
self.db.execute = wrapex(self.db.execute)
def __str__(self):
return "CloudStack %s database at %s"%(self.database,self.host)
def get_schema_level(self):
return self.get_config_value('schema.level') or cloud_utils.INITIAL_LEVEL
def set_schema_level(self,l):
self.db.execute(
"INSERT INTO configuration (category,instance,component,name,value,description) VALUES ('Hidden', 'DEFAULT', 'database', 'schema.level', %s, 'The schema level of this database') ON DUPLICATE KEY UPDATE value = %s", (l,l)
)
self.commit()
def commit(self):
self.conn.commit()
#self.conn.close()
def close(self):
self.conn.close()
def get_config_value(self,name):
self.db.execute("select value from configuration where name = %s",(name,))
try: return self.db.fetchall()[0][0]
except IndexError: return
def run_sql_resource(self,resource):
sqlfiletext = file(os.path.join(self.resourcedir,resource)).read(-1)
sqlstatements = sqlfiletext.split(";")
for stmt in sqlstatements:
if not stmt.strip(): continue # skip empty statements
self.db.execute(stmt)
class FromInitialTo21NewSchema(cloud_utils.MigrationStep):
def __str__(self): return "Altering the database schema"
from_level = cloud_utils.INITIAL_LEVEL
to_level = "2.1-01"
def run(self): self.context.run_sql_resource("schema-20to21.sql")
class From21NewSchemaTo21NewSchemaPlusIndex(cloud_utils.MigrationStep):
def __str__(self): return "Altering indexes"
from_level = "2.1-01"
to_level = "2.1-02"
def run(self): self.context.run_sql_resource("index-20to21.sql")
class From21NewSchemaPlusIndexTo21DataMigratedPart1(cloud_utils.MigrationStep):
def __str__(self): return "Performing data migration, stage 1"
from_level = "2.1-02"
to_level = "2.1-03"
def run(self): self.context.run_sql_resource("data-20to21.sql")
class From21step1toTo21datamigrated(cloud_utils.MigrationStep):
def __str__(self): return "Performing data migration, stage 2"
from_level = "2.1-03"
to_level = "2.1-04"
def run(self):
systemjars = "@SYSTEMJARS@".split()
pipe = subprocess.Popen(["build-classpath"]+systemjars,stdout=subprocess.PIPE)
systemcp,throwaway = pipe.communicate()
systemcp = systemcp.strip()
if pipe.wait(): # this means that build-classpath failed miserably
systemcp = "@SYSTEMCLASSPATH@"
pcp = os.path.pathsep.join( glob.glob( os.path.join ( "@PREMIUMJAVADIR@" , "*" ) ) )
mscp = "@MSCLASSPATH@"
depscp = "@DEPSCLASSPATH@"
migrationxml = "@SERVERSYSCONFDIR@"
conf = self.context.configdir
cp = os.path.pathsep.join([pcp,systemcp,depscp,mscp,migrationxml,conf])
cmd = ["java"]
cmd += ["-cp",cp]
cmd += ["com.cloud.migration.Db20to21MigrationUtil"]
logging.debug("Running command: %s"," ".join(cmd))
subprocess.check_call(cmd)
class From21datamigratedTo21postprocessed(cloud_utils.MigrationStep):
def __str__(self): return "Postprocessing migrated data"
from_level = "2.1-04"
to_level = "2.1"
def run(self): self.context.run_sql_resource("postprocess-20to21.sql")
class From21To213(cloud_utils.MigrationStep):
def __str__(self): return "Dropping obsolete indexes"
from_level = "2.1"
to_level = "2.1.3"
def run(self): self.context.run_sql_resource("index-212to213.sql")
class From213To22data(cloud_utils.MigrationStep):
def __str__(self): return "Migrating data"
from_level = "2.1.3"
to_level = "2.2-01"
def run(self): self.context.run_sql_resource("data-21to22.sql")
class From22dataTo22(cloud_utils.MigrationStep):
def __str__(self): return "Migrating indexes"
from_level = "2.2-01"
to_level = "2.2"
def run(self): self.context.run_sql_resource("index-21to22.sql")
# command line harness functions
def setup_logging(level):
l = logging.getLogger()
l.setLevel(level)
h = logging.StreamHandler(sys.stderr)
l.addHandler(h)
def setup_optparse():
usage = \
"""%prog [ options ... ]
This command migrates the CloudStack database."""
parser = OptionParser(usage=usage)
parser.add_option("-c", "--config", action="store", type="string",dest='configdir',
default=os.path.join("@MSCONF@"),
help="Configuration directory with a db.properties file, pointing to the CloudStack database")
parser.add_option("-r", "--resourcedir", action="store", type="string",dest='resourcedir',
default="@SETUPDATADIR@",
help="Resource directory with database SQL files used by the migration process")
parser.add_option("-d", "--debug", action="store_true", dest='debug',
default=False,
help="Increase log level from INFO to DEBUG")
parser.add_option("-e", "--dump-evolvers", action="store_true", dest='dumpevolvers',
default=False,
help="Dump evolvers in the order they would be executed, but do not run them")
#parser.add_option("-n", "--dry-run", action="store_true", dest='dryrun',
#default=False,
#help="Run the process as it would normally run, but do not commit the final transaction, so database changes are never saved")
parser.add_option("-f", "--start-at-level", action="store", type="string",dest='fromlevel',
default=None,
help="Rather than discovering the database schema level to start from, start migration from this level. The special value '-' (a dash without quotes) represents the earliest schema level")
parser.add_option("-t", "--end-at-level", action="store", type="string",dest='tolevel',
default=None,
help="Rather than evolving the database to the most up-to-date level, end migration at this level")
return parser
def main(*args):
"""The entry point of this program"""
parser = setup_optparse()
opts, args = parser.parse_args(*args)
if args: parser.error("This command accepts no parameters")
if opts.debug: loglevel = logging.DEBUG
else: loglevel = logging.INFO
setup_logging(loglevel)
# FIXME implement
opts.dryrun = False
configdir = opts.configdir
resourcedir = opts.resourcedir
try:
props = cloud_utils.read_properties(os.path.join(configdir,'db.properties'))
except (IOError,OSError) as e:
logging.error("Cannot read from config file: %s",e)
logging.error("You may want to point to a specific config directory with the --config= option")
return 2
if not os.path.isdir(resourcedir):
logging.error("Cannot find directory with SQL files %s",resourcedir)
logging.error("You may want to point to a specific resource directory with the --resourcedir= option")
return 2
host = props["db.cloud.host"]
port = int(props["db.cloud.port"])
username = props["db.cloud.username"]
password = props["db.cloud.password"]
database = props["db.cloud.name"]
# tell the migrator to load its steps from the globals list
migrator = cloud_utils.Migrator(list(globals().values()))
if opts.dumpevolvers:
print("Evolution steps:")
print(" %s %s %s"%("From","To","Evolver in charge"))
for f,t,e in migrator.get_evolver_chain():
print(" %s %s %s"%(f,t,e))
return
#initialize a context with the read configuration
context = CloudContext(host=host,port=port,username=username,password=password,database=database,configdir=configdir,resourcedir=resourcedir)
try:
try:
migrator.run(context,dryrun=opts.dryrun,starting_level=opts.fromlevel,ending_level=opts.tolevel)
finally:
context.close()
except (cloud_utils.NoMigrationPath,cloud_utils.NoMigrator) as e:
logging.error("%s",e)
return 4
if __name__ == "__main__":
retval = main()
if retval: sys.exit(retval)
else: sys.exit()
exit $res

View File

@ -67,7 +67,7 @@ class DBDeployer(object):
dbDotProperties = {}
dbDotPropertiesIndex = 0
encryptionKeyFile = '@MSCONF@/key'
encryptionJarPath = '@COMMONLIBDIR@/lib/jasypt-1.9.3.jar'
encryptionJarPath = '@COMMONLIBDIR@/lib/cloudstack-utils.jar'
success = False
magicString = 'This_is_a_magic_string_i_think_no_one_will_duplicate'
tmpMysqlFile = os.path.join(os.path.expanduser('~/'), 'cloudstackmysql.tmp.sql')
@ -391,8 +391,8 @@ for example:
checkSELinux()
def processEncryptionStuff(self):
def encrypt(input):
cmd = ['java','-Djava.security.egd=file:/dev/urandom','-classpath','"' + self.encryptionJarPath + '"','org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI', 'encrypt.sh', 'input=\'%s\''%input, 'password=\'%s\''%self.mgmtsecretkey,'verbose=false']
def encrypt(value):
cmd = ['java','-classpath','"' + self.encryptionJarPath + '"','com.cloud.utils.crypt.EncryptionCLI','-i','"' + value + '"', '-p', '"' + self.mgmtsecretkey + '"', self.encryptorVersion]
return str(runCmd(cmd)).strip('\r\n')
def saveMgmtServerSecretKey():
@ -408,6 +408,7 @@ for example:
def encryptDBSecretKey():
self.putDbProperty('db.cloud.encrypt.secret', formatEncryptResult(encrypt(self.dbsecretkey)))
self.putDbProperty("db.cloud.encryptor.version", self.options.encryptorVersion)
def encryptDBPassword():
dbPassword = self.getDbProperty('db.cloud.password')
@ -450,6 +451,11 @@ for example:
self.info("Using specified cluster management server node IP %s" % self.options.mshostip, True)
self.encryptiontype = self.options.encryptiontype
if self.encryptiontype == "env":
self.mgmtsecretkey = os.getenv("CLOUD_SECRET_KEY")
if not self.mgmtsecretkey:
self.errorAndExit("Please set environment variable CLOUD_SECRET_KEY if the encryption type is 'env'")
else:
self.mgmtsecretkey = self.options.mgmtsecretkey
self.dbsecretkey = self.options.dbsecretkey
self.isDebug = self.options.debug
@ -464,6 +470,11 @@ for example:
if self.options.mysqlbinpath:
self.mysqlBinPath = self.options.mysqlbinpath
if self.options.encryptorVersion:
self.encryptorVersion = "--encryptorversion %s" % self.options.encryptorVersion
else:
self.encryptorVersion = ""
def parseUserAndPassword(cred):
stuff = cred.split(':')
if len(stuff) != 1 and len(stuff) != 2:
@ -524,11 +535,11 @@ for example:
def validateParameters():
if self.options.schemaonly and self.rootuser != None:
self.errorAndExit("--schema-only and --deploy-as cannot be passed together\n")
if self.encryptiontype != 'file' and self.encryptiontype != 'web':
self.errorAndExit('Wrong encryption type %s, --encrypt-type can only be "file" or "web'%self.encryptiontype)
if self.encryptiontype != 'file' and self.encryptiontype != 'web' and self.encryptiontype != 'env':
self.errorAndExit('Wrong encryption type %s, --encrypt-type can only be "file" or "web" or "env"' % self.encryptiontype)
#---------------------- option parsing and command line checks ------------------------
usage = """%prog user:[password]@mysqlhost:[port] [--deploy-as=rootuser:[rootpassword]] [--auto=/path/to/server-setup.xml] [-e ENCRYPTIONTYPE] [-m MGMTSECRETKEY] [-k DBSECRETKEY] [--debug]
usage = """%prog user:[password]@mysqlhost:[port] [--deploy-as=rootuser:[rootpassword]] [--auto=/path/to/server-setup.xml] [-e ENCRYPTIONTYPE] [-m MGMTSECRETKEY] [-k DBSECRETKEY] [-g ENCRYPTORVERSION] [--debug]
This command sets up the CloudStack Management Server and CloudStack Usage Server database configuration (connection credentials and host information) based on the first argument.
@ -538,6 +549,8 @@ for example:
The port and the password are optional and can be left out.. If host is omitted altogether, it will default to localhost.
The encryptor version is optional. The options are V1 and V2. If it is not set, the default encryptor will be used.
Examples:
%prog cloud:secret
@ -553,10 +566,13 @@ for example:
with password 'nonsense', and recreates the databases, creating
the user alex with password 'founder' as necessary
%prog alex:founder@1.2.3.4 --deploy-as=root:nonsense -e file -m password -k dbpassword
%prog alex:founder@1.2.3.4 --deploy-as=root:nonsense -e file -m password -k dbpassword -g V2
In addition actions performing in above example, using 'password' as management server encryption key
and 'dbpassword' as database encryption key, saving management server encryption key to a file as the
encryption type specified by -e is file.
The credentials in @MSCONF@/db.properties are encrypted by encryptor V2 (AeadBase64Encryptor).
The db.cloud.encryptor.version is also set to V2. Sensitive values in cloudstack databases will be
encrypted by the encryptor V2 using the database encryption key.
%prog alena:tests@5.6.7.8 --deploy-as=root:nonsense --auto=/root/server-setup.xml
sets alena up as the MySQL user, then connects as the root user
@ -577,7 +593,7 @@ for example:
self.parser.add_option("-a", "--auto", action="store", type="string", dest="serversetup", default="",
help="Path to an XML file describing an automated unattended cloud setup")
self.parser.add_option("-e", "--encrypt-type", action="store", type="string", dest="encryptiontype", default="file",
help="Encryption method used for db password encryption. Valid values are file, web. Default is file.")
help="Encryption method used for db password encryption. Valid values are file, web and env. Default is file.")
self.parser.add_option("-m", "--managementserver-secretkey", action="store", type="string", dest="mgmtsecretkey", default="password",
help="Secret key used to encrypt confidential parameters in db.properties. A string, default is password")
self.parser.add_option("-k", "--database-secretkey", action="store", type="string", dest="dbsecretkey", default="password",
@ -588,8 +604,10 @@ for example:
help="Region Id for the management server cluster")
self.parser.add_option("-c", "--db-conf-path", action="store", dest="dbConfPath", help="The path to find db.properties which hold db properties")
self.parser.add_option("-f", "--db-files-path", action="store", dest="dbFilesPath", help="The path to find sql files to create initial database(s)")
self.parser.add_option("-j", "--encryption-jar-path", action="store", dest="encryptionJarPath", help="The path to the jasypt library to be used to encrypt the values in db.properties")
self.parser.add_option("-j", "--encryption-jar-path", action="store", dest="encryptionJarPath", help="The cloudstack jar to be used to encrypt the values in db.properties")
self.parser.add_option("-n", "--encryption-key-file", action="store", dest="encryptionKeyFile", help="The name of the file in which encryption key to be generated")
self.parser.add_option("-g", "--encryptor-version", action="store", dest="encryptorVersion", default="V2",
help="The encryptor version to be used to encrypt the values in db.properties")
self.parser.add_option("-b", "--mysql-bin-path", action="store", dest="mysqlbinpath", help="The mysql installed bin path")
(self.options, self.args) = self.parser.parse_args()
parseCasualCredit()

View File

@ -63,7 +63,7 @@ class DBDeployer(object):
dbDotProperties = {}
dbDotPropertiesIndex = 0
encryptionKeyFile = '@MSCONF@/key'
encryptionJarPath = '@COMMONLIBDIR@/lib/jasypt-1.9.3.jar'
encryptionJarPath = '@COMMONLIBDIR@/lib/cloudstack-utils.jar'
success = False
magicString = 'This_is_a_magic_string_i_think_no_one_will_duplicate'
@ -183,8 +183,8 @@ for example:
self.success = True # At here, we have done successfully and nothing more after this flag is set
def processEncryptionStuff(self):
def encrypt(input):
cmd = ['java','-classpath',self.encryptionJarPath,'org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI', 'encrypt.sh', 'input=\'%s\''%input, 'password=%s'%self.mgmtsecretkey,'verbose=false']
def encrypt(value):
cmd = ['java','-classpath','"' + self.encryptionJarPath + '"','com.cloud.utils.crypt.EncryptionCLI','-i','"' + value + '"', '-p', '"' + self.mgmtsecretkey + '"']
return runCmd(cmd).strip('\n')
def saveMgmtServerSecretKey():

View File

@ -27,7 +27,7 @@ from marvin.lib.decoratorGenerators import skipTestIf
from marvin.lib.utils import *
from nose.plugins.attrib import attr
_multiprocess_shared_ = True
_multiprocess_shared_ = False
class TestPrimaryStorageServices(cloudstackTestCase):
@ -51,6 +51,12 @@ class TestPrimaryStorageServices(cloudstackTestCase):
if self.template == FAILED:
assert False, "get_suitable_test_template() failed to return template with description %s" % self.services["ostype"]
self.excluded_pools = []
storage_pool_list = StoragePool.list(self.apiclient, zoneid=self.zone.id)
for pool in storage_pool_list:
if pool.state != 'Up':
self.excluded_pools.append(pool.id)
return
def tearDown(self):
@ -301,6 +307,8 @@ class TestPrimaryStorageServices(cloudstackTestCase):
for pool in storage_pool_list:
if (pool.id == storage_pool_2.id):
continue
if pool.id in self.excluded_pools:
continue
StoragePool.update(self.apiclient, id=pool.id, enabled=False)
# deployvm
@ -334,6 +342,8 @@ class TestPrimaryStorageServices(cloudstackTestCase):
for pool in storage_pool_list:
if (pool.id == storage_pool_2.id):
continue
if pool.id in self.excluded_pools:
continue
StoragePool.update(self.apiclient, id=pool.id, enabled=True)
# Enable all hosts
for host in list_hosts_response:
@ -582,6 +592,8 @@ class TestStorageTags(cloudstackTestCase):
)
self.debug("VM-1 Volumes: %s" % vm_1_volumes)
self.assertEqual(vm_1_volumes[0].id, self.volume_1.id, "Check that volume V-1 has been attached to VM-1")
time.sleep(30)
self.virtual_machine_1.detach_volume(self.apiclient, self.volume_1)
return

View File

@ -41,7 +41,8 @@ dbHost=
dbUser=
dbPassword=
name=
jasypt='/usr/share/cloudstack-common/lib/jasypt-1.9.0.jar'
jarfile='/usr/share/cloudstack-common/lib/cloudstack-utils.jar'
while getopts 'm:h:f:u:Ft:e:s:o:r:d:n:' OPTION
do
case $OPTION in
@ -134,7 +135,7 @@ then
encPassword=$(sed '/^\#/d' /etc/cloudstack/management/db.properties | grep 'db.cloud.password' | tail -n 1 | cut -d "=" -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'i | sed 's/^ENC(\(.*\))/\1/')
if [ ! $encPassword == "" ]
then
dbPassword=(`java -classpath $jasypt org.jasypt.intf.cli.JasyptPBEStringDecryptionCLI decrypt.sh input=$encPassword password=$msKey verbose=false`)
dbPassword=(`java -classpath $jarfile com.cloud.utils.crypt.EncryptionCLI -d -i "$encPassword" -p "$msKey"`)
if [ ! $dbPassword ]
then
echo "Failed to decrypt DB password from db.properties"

View File

@ -95,6 +95,7 @@ import com.cloud.utils.db.GlobalLock;
import com.cloud.utils.db.QueryBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.exception.CloudRuntimeException;
import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
@ -208,10 +209,17 @@ public class UsageManagerImpl extends ManagerBase implements UsageManager, Runna
s_logger.info("Implementation Version is " + _version);
}
Map<String, String> configs = _configDao.getConfiguration(params);
Map<String, String> configs;
try {
configs = _configDao.getConfiguration(params);
if (params != null) {
mergeConfigs(configs, params);
s_logger.info("configs = " + configs);
}
} catch (CloudRuntimeException e) {
s_logger.error("Unhandled configuration exception: " + e.getMessage());
throw new CloudRuntimeException("Unhandled configuration exception", e);
}
String execTime = configs.get("usage.stats.job.exec.time");

View File

@ -210,6 +210,16 @@
<artifactId>nashorn-core</artifactId>
<version>15.3</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>${cs.commons-cli.version}</version>
</dependency>
<dependency>
<groupId>com.google.crypto.tink</groupId>
<artifactId>tink</artifactId>
<version>${cs.tink.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
@ -234,6 +244,34 @@
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>rebuild-war</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<artifactSet>
<includes>
<include>ch.qos.reload4j</include>
<include>com.google.crypto.tink:tink</include>
<include>com.google.protobuf:protobuf-java</include>
<include>commons-cli:commons-cli</include>
<include>commons-codec:commons-codec</include>
<include>org.apache.commons:commons-lang3</include>
<include>org.jasypt:jasypt</include>
</includes>
</artifactSet>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>

View File

@ -18,11 +18,10 @@
*/
package com.cloud.utils;
import com.cloud.utils.crypt.CloudStackEncryptor;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
import org.jasypt.encryption.pbe.PBEStringEncryptor;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@ -32,13 +31,10 @@ import java.security.NoSuchAlgorithmException;
public class EncryptionUtil {
public static final Logger s_logger = Logger.getLogger(EncryptionUtil.class.getName());
private static PBEStringEncryptor encryptor;
private static CloudStackEncryptor encryptor;
private static void initialize(String key) {
StandardPBEStringEncryptor standardPBEStringEncryptor = new StandardPBEStringEncryptor();
standardPBEStringEncryptor.setAlgorithm("PBEWITHSHA1ANDDESEDE");
standardPBEStringEncryptor.setPassword(key);
encryptor = standardPBEStringEncryptor;
encryptor = new CloudStackEncryptor(key, null, EncryptionUtil.class);
}
public static String encodeData(String data, String key) {

View File

@ -71,4 +71,5 @@ public interface SerialVersionUID {
public static final long UnavailableCommandException = Base | 0x2f;
public static final long OriginDeniedException = Base | 0x30;
public static final long StorageAccessException = Base | 0x31;
public static final long EncryptionException = Base | 0x32;
}

View File

@ -0,0 +1,63 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.cloud.utils.crypt;
import com.google.crypto.tink.Aead;
import com.google.crypto.tink.aead.AeadConfig;
import com.google.crypto.tink.subtle.AesGcmJce;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
public class AeadBase64Encryptor implements Base64Encryptor {
Aead aead = null;
private final byte[] aad = new byte[]{};
public AeadBase64Encryptor(byte[] key) {
try {
AeadConfig.register();
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(key);
this.aead = new AesGcmJce(hash);
} catch (Exception e) {
throw new EncryptionException("Failed to initialize AeadBase64Encryptor");
}
}
@Override
public String encrypt(String plain) {
try {
return Base64.getEncoder().encodeToString(aead.encrypt(plain.getBytes(StandardCharsets.UTF_8), aad));
} catch (Exception ex) {
throw new EncryptionException("Failed to encrypt " + plain + ". Error: " + ex.getMessage());
}
}
@Override
public String decrypt(String encrypted) {
try {
return new String(aead.decrypt(Base64.getDecoder().decode(encrypted), aad));
} catch (Exception ex) {
throw new EncryptionException("Failed to decrypt " + CloudStackEncryptor.hideValueWithAsterisks(encrypted) + ". Error: " + ex.getMessage());
}
}
}

View File

@ -0,0 +1,27 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.cloud.utils.crypt;
public interface Base64Encryptor {
String encrypt(String plain);
String decrypt(String encrypted);
}

View File

@ -0,0 +1,147 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.cloud.utils.crypt;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.cloud.utils.exception.CloudRuntimeException;
public class CloudStackEncryptor {
public static final Logger s_logger = Logger.getLogger(CloudStackEncryptor.class);
private Base64Encryptor encryptor = null;
private LegacyBase64Encryptor encryptorV1;
private AeadBase64Encryptor encryptorV2;
String password;
EncryptorVersion version;
Class callerClass;
enum EncryptorVersion {
V1, V2;
public static EncryptorVersion fromString(String version) {
if (version != null && version.equalsIgnoreCase("v1")) {
return V1;
}
if (version != null && version.equalsIgnoreCase("v2")) {
return V2;
}
if (StringUtils.isNotEmpty(version)) {
throw new CloudRuntimeException(String.format("Invalid encryptor version: %s, valid options are: %s", version,
Arrays.stream(EncryptorVersion.values()).map(EncryptorVersion::name).collect(Collectors.joining(","))));
}
return null;
}
public static EncryptorVersion defaultVersion() {
return V2;
}
}
public CloudStackEncryptor(String password, String version, Class callerClass) {
this.password = password;
this.version = EncryptorVersion.fromString(version);
this.callerClass = callerClass;
initialize();
}
public String encrypt(String plain) {
if (StringUtils.isEmpty(plain)) {
return plain;
}
try {
if (encryptor == null) {
encryptor = encryptorV2;
s_logger.debug(String.format("CloudStack will encrypt and decrypt values using default encryptor : %s for class %s",
encryptor.getClass().getSimpleName(), callerClass.getSimpleName()));
}
return encryptor.encrypt(plain);
} catch (EncryptionException e) {
throw new CloudRuntimeException("Error encrypting value: ", e);
}
}
public String decrypt(String encrypted) {
if (StringUtils.isEmpty(encrypted)) {
return encrypted;
}
if (encryptor != null) {
try {
return encryptor.decrypt(encrypted);
} catch (EncryptionException e) {
throw new CloudRuntimeException("Error decrypting value: " + hideValueWithAsterisks(encrypted), e);
}
} else {
String result = decrypt(encryptorV2, encrypted);
if (result != null) {
return result;
}
result = decrypt(encryptorV1, encrypted);
if (result != null) {
return result;
}
throw new CloudRuntimeException("Failed to decrypt value: " + hideValueWithAsterisks(encrypted));
}
}
private String decrypt(Base64Encryptor encryptorToUse, String encrypted) {
try {
String result = encryptorToUse.decrypt(encrypted);
s_logger.debug(String.format("CloudStack will encrypt and decrypt values using encryptor : %s for class %s",
encryptorToUse.getClass().getSimpleName(), callerClass.getSimpleName()));
encryptor = encryptorToUse;
return result;
} catch (EncryptionException ex) {
s_logger.warn(String.format("Failed to decrypt value using %s: %s", encryptorToUse.getClass().getSimpleName(), hideValueWithAsterisks(encrypted)));
}
return null;
}
protected static String hideValueWithAsterisks(String value) {
if (StringUtils.isEmpty(value)) {
return value;
}
int numChars = value.length() >= 10 ? 5: 1;
int numAsterisks = 10 - numChars;
return value.substring(0, numChars) + "*".repeat(numAsterisks);
}
protected void initialize() {
s_logger.debug("Calling to initialize for class " + callerClass.getName());
encryptor = null;
if (EncryptorVersion.V1.equals(version)) {
encryptorV1 = new LegacyBase64Encryptor(password);
encryptor = encryptorV1;
s_logger.debug("Initialized with encryptor : " + encryptorV1.getClass().getSimpleName());
} else if (EncryptorVersion.V2.equals(version)) {
encryptorV2 = new AeadBase64Encryptor(password.getBytes(StandardCharsets.UTF_8));
encryptor = encryptorV2;
s_logger.debug("Initialized with encryptor : " + encryptorV2.getClass().getSimpleName());
} else {
encryptorV1 = new LegacyBase64Encryptor(password);
encryptorV2 = new AeadBase64Encryptor(password.getBytes(StandardCharsets.UTF_8));
s_logger.debug("Initialized with all possible encryptors");
}
}
}

View File

@ -22,16 +22,13 @@ package com.cloud.utils.crypt;
import java.util.Properties;
import org.apache.log4j.Logger;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
import com.cloud.utils.db.DbProperties;
import com.cloud.utils.exception.CloudRuntimeException;
public class DBEncryptionUtil {
public static final Logger s_logger = Logger.getLogger(DBEncryptionUtil.class);
private static StandardPBEStringEncryptor s_encryptor = null;
private static CloudStackEncryptor s_encryptor = null;
public static String encrypt(String plain) {
if (!EncryptionSecretKeyChecker.useEncryption() || (plain == null) || plain.isEmpty()) {
@ -40,14 +37,7 @@ public class DBEncryptionUtil {
if (s_encryptor == null) {
initialize();
}
String encryptedString = null;
try {
encryptedString = s_encryptor.encrypt(plain);
} catch (EncryptionOperationNotPossibleException e) {
s_logger.debug("Error while encrypting: " + plain);
throw e;
}
return encryptedString;
return s_encryptor.encrypt(plain);
}
public static String decrypt(String encrypted) {
@ -58,17 +48,11 @@ public class DBEncryptionUtil {
initialize();
}
String plain = null;
try {
plain = s_encryptor.decrypt(encrypted);
} catch (EncryptionOperationNotPossibleException e) {
s_logger.debug("Error while decrypting: " + encrypted);
throw e;
}
return plain;
return s_encryptor.decrypt(encrypted);
}
private static void initialize() {
protected static void initialize() {
s_logger.debug("Calling to initialize");
final Properties dbProps = DbProperties.getDbProperties();
if (EncryptionSecretKeyChecker.useEncryption()) {
@ -76,12 +60,12 @@ public class DBEncryptionUtil {
if (dbSecretKey == null || dbSecretKey.isEmpty()) {
throw new CloudRuntimeException("Empty DB secret key in db.properties");
}
String dbEncryptorVersion = dbProps.getProperty("db.cloud.encryptor.version");
s_encryptor = new StandardPBEStringEncryptor();
s_encryptor.setAlgorithm("PBEWithMD5AndDES");
s_encryptor.setPassword(dbSecretKey);
s_encryptor = new CloudStackEncryptor(dbSecretKey, dbEncryptorVersion, DBEncryptionUtil.class);
} else {
throw new CloudRuntimeException("Trying to encrypt db values when encrytion is not enabled");
throw new CloudRuntimeException("Trying to encrypt db values when encryption is not enabled");
}
s_logger.debug("initialized");
}
}

View File

@ -0,0 +1,80 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.cloud.utils.crypt;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
public class EncryptionCLI {
private static final String VERBOSE_OPTION = "verbose";
private static final String DECRYPT_OPTION = "decrypt";
private static final String INPUT_OPTION = "input";
private static final String PASSWORD_OPTION = "password";
private static final String ENCRYPTOR_VERSION_OPTION = "encryptorversion";
public static void main(String[] args) throws ParseException {
Options options = new Options();
Option verbose = Option.builder("v").longOpt(VERBOSE_OPTION).argName(VERBOSE_OPTION).required(false).desc("Verbose output").hasArg(false).build();
Option decrypt = Option.builder("d").longOpt(DECRYPT_OPTION).argName(DECRYPT_OPTION).required(false).desc("Decrypt instead of encrypt").hasArg(false).build();
Option input = Option.builder("i").longOpt(INPUT_OPTION).argName(INPUT_OPTION).required(true).hasArg().desc("The input string to process").build();
Option password = Option.builder("p").longOpt(PASSWORD_OPTION).argName(PASSWORD_OPTION).required(true).hasArg().desc("The encryption password").build();
Option encryptorVersion = Option.builder("e").longOpt(ENCRYPTOR_VERSION_OPTION).argName(ENCRYPTOR_VERSION_OPTION).required(false).hasArg().desc("The encryptor version").build();
options.addOption(verbose);
options.addOption(decrypt);
options.addOption(input);
options.addOption(password);
options.addOption(encryptorVersion);
CommandLine cmdLine = null;
CommandLineParser parser = new DefaultParser();
HelpFormatter helper = new HelpFormatter();
try {
cmdLine = parser.parse(options, args);
} catch (ParseException ex) {
System.out.println(ex.getMessage());
helper.printHelp("Usage:", options);
System.exit(1);
}
CloudStackEncryptor encryptor = new CloudStackEncryptor(cmdLine.getOptionValue(PASSWORD_OPTION),
cmdLine.getOptionValue(encryptorVersion), EncryptionCLI.class);
String result;
String inString = cmdLine.getOptionValue(INPUT_OPTION);
if (cmdLine.hasOption(DECRYPT_OPTION)) {
result = encryptor.decrypt(inString);
} else {
result = encryptor.encrypt(inString);
}
if (cmdLine.hasOption(VERBOSE_OPTION)) {
System.out.printf("Input: %s%n", inString);
System.out.printf("Encrypted: %s%n", result);
} else {
System.out.printf("%s%n", result);
}
}
}

View File

@ -0,0 +1,34 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.cloud.utils.crypt;
import com.cloud.utils.SerialVersionUID;
public class EncryptionException extends RuntimeException {
private static final long serialVersionUID = SerialVersionUID.EncryptionException;
public EncryptionException(String message) {
super(message);
}
public EncryptionException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -26,13 +26,12 @@ import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;
import java.util.Properties;
import javax.annotation.PostConstruct;
import org.apache.log4j.Logger;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
import com.cloud.utils.db.DbProperties;
import com.cloud.utils.exception.CloudRuntimeException;
@ -45,7 +44,7 @@ public class EncryptionSecretKeyChecker {
private static final String s_altKeyFile = "key";
private static final String s_keyFile = "key";
private static final String s_envKey = "CLOUD_SECRET_KEY";
private static StandardPBEStringEncryptor s_encryptor = new StandardPBEStringEncryptor();
private static CloudStackEncryptor s_encryptor = null;
private static boolean s_useEncryption = false;
@PostConstruct
@ -69,11 +68,8 @@ public class EncryptionSecretKeyChecker {
return;
}
s_encryptor.setAlgorithm("PBEWithMD5AndDES");
String secretKey = null;
SimpleStringPBEConfig stringConfig = new SimpleStringPBEConfig();
if (encryptionType.equals("file")) {
InputStream is = this.getClass().getClassLoader().getResourceAsStream(s_keyFile);
if (is == null) {
@ -122,12 +118,14 @@ public class EncryptionSecretKeyChecker {
throw new CloudRuntimeException("Invalid encryption type: " + encryptionType);
}
stringConfig.setPassword(secretKey);
s_encryptor.setConfig(stringConfig);
s_useEncryption = true;
if (secretKey == null) {
throw new CloudRuntimeException("null secret key is found when setting up server encryption");
}
public static StandardPBEStringEncryptor getEncryptor() {
initEncryptor(secretKey);
}
public static CloudStackEncryptor getEncryptor() {
return s_encryptor;
}
@ -135,12 +133,36 @@ public class EncryptionSecretKeyChecker {
return s_useEncryption;
}
//Initialize encryptor for migration during secret key change
public static void initEncryptorForMigration(String secretKey) {
s_encryptor.setAlgorithm("PBEWithMD5AndDES");
SimpleStringPBEConfig stringConfig = new SimpleStringPBEConfig();
stringConfig.setPassword(secretKey);
s_encryptor.setConfig(stringConfig);
public static void initEncryptor(String secretKey) {
s_encryptor = new CloudStackEncryptor(secretKey, null, EncryptionSecretKeyChecker.class);
s_useEncryption = true;
}
public static void resetEncryptor() {
s_encryptor = null;
s_useEncryption = false;
}
protected static String decryptPropertyIfNeeded(String value) {
if (s_encryptor == null) {
throw new CloudRuntimeException("encryptor not initialized");
}
if (value.startsWith("ENC(") && value.endsWith(")")) {
String inner = value.substring("ENC(".length(), value.length() - ")".length());
return s_encryptor.decrypt(inner);
}
return value;
}
public static void decryptAnyProperties(Properties properties) {
if (s_encryptor == null) {
throw new CloudRuntimeException("encryptor not initialized");
}
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String value = (String) entry.getValue();
properties.replace(entry.getKey(), decryptPropertyIfNeeded(value));
}
}
}

View File

@ -0,0 +1,58 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.cloud.utils.crypt;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
public class LegacyBase64Encryptor implements Base64Encryptor {
StandardPBEStringEncryptor encryptor;
public LegacyBase64Encryptor(String password) {
try {
encryptor = new StandardPBEStringEncryptor();
encryptor.setAlgorithm("PBEWithMD5AndDES");
encryptor.setPassword(password);
SimpleStringPBEConfig stringConfig = new SimpleStringPBEConfig();
encryptor.setConfig(stringConfig);
} catch (Exception e) {
throw new EncryptionException("Failed to initialize LegacyBase64Encryptor");
}
}
@Override
public String encrypt(String plain) {
try {
return encryptor.encrypt(plain);
} catch (Exception ex) {
throw new EncryptionException("Failed to encrypt " + plain + ". Error: " + ex.getMessage());
}
}
@Override
public String decrypt(String encrypted) {
try {
return encryptor.decrypt(encrypted);
} catch (Exception ex) {
throw new EncryptionException("Failed to decrypt " + CloudStackEncryptor.hideValueWithAsterisks(encrypted) + ". Error: " + ex.getMessage());
}
}
}

View File

@ -27,14 +27,11 @@ import java.util.Properties;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.jasypt.properties.EncryptableProperties;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.crypt.EncryptionSecretKeyChecker;
public class DbProperties {
private static final Logger log = Logger.getLogger(DbProperties.class);
private static Properties properties = new Properties();
@ -46,11 +43,12 @@ public class DbProperties {
checker.check(dbProps, dbEncryptionType);
if (EncryptionSecretKeyChecker.useEncryption()) {
log.debug("encryptionsecretkeychecker using encryption");
EncryptionSecretKeyChecker.decryptAnyProperties(dbProps);
return dbProps;
} else {
EncryptableProperties encrProps = new EncryptableProperties(EncryptionSecretKeyChecker.getEncryptor());
encrProps.putAll(dbProps);
return encrProps;
log.debug("encryptionsecretkeychecker not using encryption");
return dbProps;
}
}
@ -81,12 +79,10 @@ public class DbProperties {
checker.check(dbProps, dbEncryptionType);
if (EncryptionSecretKeyChecker.useEncryption()) {
StandardPBEStringEncryptor encryptor = EncryptionSecretKeyChecker.getEncryptor();
EncryptableProperties encrDbProps = new EncryptableProperties(encryptor);
encrDbProps.putAll(dbProps);
dbProps = encrDbProps;
EncryptionSecretKeyChecker.decryptAnyProperties(dbProps);
}
} catch (IOException e) {
log.error(String.format("Failed to load DB properties: %s", e.getMessage()), e);
throw new IllegalStateException("Failed to load db.properties", e);
} finally {
IOUtils.closeQuietly(is);
@ -94,6 +90,8 @@ public class DbProperties {
properties = dbProps;
loaded = true;
} else {
log.debug("DB properties were already loaded");
}
return properties;

View File

@ -19,8 +19,6 @@ package com.cloud.utils.server;
import com.cloud.utils.crypt.EncryptionSecretKeyChecker;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.jasypt.properties.EncryptableProperties;
import java.io.IOException;
import java.io.InputStream;
@ -43,10 +41,7 @@ public class ServerProperties {
checker.check(serverProps, passwordEncryptionType);
if (EncryptionSecretKeyChecker.useEncryption()) {
StandardPBEStringEncryptor encryptor = EncryptionSecretKeyChecker.getEncryptor();
EncryptableProperties encrServerProps = new EncryptableProperties(encryptor);
encrServerProps.putAll(serverProps);
serverProps = encrServerProps;
EncryptionSecretKeyChecker.decryptAnyProperties(serverProps);
}
} catch (IOException e) {
throw new IllegalStateException("Failed to load server.properties", e);

View File

@ -0,0 +1,58 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.cloud.utils.crypt;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.Properties;
public class EncryptionSecretKeyCheckerTest {
@Before
public void setup() {
EncryptionSecretKeyChecker.initEncryptor("managementkey");
}
@After
public void tearDown() {
EncryptionSecretKeyChecker.resetEncryptor();
}
@Test
public void decryptPropertyIfNeededTest() {
String rawValue = "ENC(iYVsCZXiGiC6SzZLMNBvBL93hoUpntxkuRjyaqC8L+JYKXw=)";
String result = EncryptionSecretKeyChecker.decryptPropertyIfNeeded(rawValue);
Assert.assertEquals("encthis", result);
}
@Test
public void decryptAnyPropertiesTest() {
Properties props = new Properties();
props.setProperty("db.cloud.encrypt.secret", "ENC(iYVsCZXiGiC6SzZLMNBvBL93hoUpntxkuRjyaqC8L+JYKXw=)");
props.setProperty("other.unencrypted", "somevalue");
EncryptionSecretKeyChecker.decryptAnyProperties(props);
Assert.assertEquals("encthis", props.getProperty("db.cloud.encrypt.secret"));
Assert.assertEquals("somevalue", props.getProperty("other.unencrypted"));
}
}