/**
* 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 org.apache.ambari.server.security.encryption;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.utils.AmbariPath;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.net.ntp.TimeStamp;
public class MasterKeyServiceImpl implements MasterKeyService {
private static final Log LOG = LogFactory.getLog(MasterKeyServiceImpl.class);
private static final String MASTER_PASSPHRASE = "masterpassphrase";
private static final String MASTER_PERSISTENCE_TAG_PREFIX = "#1.0# ";
private static final AESEncryptor aes = new AESEncryptor(MASTER_PASSPHRASE);
private char[] master = null;
/**
* Constructs a new MasterKeyServiceImpl using a master key read from a file.
*
* @param masterKeyFile the location of the master key file
*/
public MasterKeyServiceImpl(File masterKeyFile) {
if (masterKeyFile == null) {
throw new IllegalArgumentException("Master Key location not provided.");
}
if (masterKeyFile.exists()) {
if (isMasterKeyFile(masterKeyFile)) {
try {
initializeFromFile(masterKeyFile);
} catch (Exception e) {
LOG.error(String.format("Cannot initialize master key from %s: %s", masterKeyFile.getAbsolutePath(), e.getLocalizedMessage()), e);
}
} else {
LOG.error(String.format("The file at %s is not a master ket file", masterKeyFile.getAbsolutePath()));
}
} else {
LOG.error(String.format("Cannot open master key file, %s", masterKeyFile.getAbsolutePath()));
}
}
/**
* Constructs a new MasterKeyServiceImpl using the specified master key.
*
* @param masterKey the master key
*/
public MasterKeyServiceImpl(String masterKey) {
if (masterKey != null) {
master = masterKey.toCharArray();
} else {
throw new IllegalArgumentException("Master key cannot be null");
}
}
/**
* Constructs a new MasterKeyServiceImpl using the master key found in the environment.
*/
public MasterKeyServiceImpl() {
String key = readMasterKey();
if (key != null) {
master = key.toCharArray();
}
}
@Override
public boolean isMasterKeyInitialized() {
return master != null;
}
@Override
public char[] getMasterSecret() {
return master;
}
public static void main(String args[]) {
String masterKey = "ThisissomeSecretPassPhrasse";
String masterKeyLocation = AmbariPath.getPath("/var/lib/ambari-server/keys/master");
boolean persistMasterKey = false;
if (args != null && args.length > 0) {
masterKey = args[0];
if (args.length > 1) {
masterKeyLocation = args[1];
}
if (args.length > 2 && !args[2].isEmpty()) {
persistMasterKey = args[2].toLowerCase().equals("true");
}
}
if (persistMasterKey && !MasterKeyServiceImpl.initializeMasterKeyFile(new File(masterKeyLocation), masterKey)) {
System.exit(1);
} else {
System.exit(0);
}
}
/**
* Initializes the master key file.
* <p/>
* If the specified file already exists, it it tested to see if it is a master key file. If so, it
* will be truncated and the new master key will be stored in the new file. If the file appears
* to not be a master key file,no changes will be made. The user must manually remove the file if
* deemed appropriate.
*
* @param masterKeyFile the file to write the master key to
* @param masterKey the master key
* @return true if the master key was written to the specified file; otherwise false
*/
public static boolean initializeMasterKeyFile(File masterKeyFile, String masterKey) {
LOG.debug(String.format("Persisting master key into %s", masterKeyFile.getAbsolutePath()));
EncryptionResult atom = null;
if (masterKey != null) {
try {
atom = aes.encrypt(masterKey);
} catch (Exception e) {
LOG.error(String.format("Failed to encrypt master key, no changes have been made: %s", e.getLocalizedMessage()), e);
return false;
}
}
if (masterKeyFile.exists()) {
if ((masterKeyFile.length() == 0) || isMasterKeyFile(masterKeyFile)) {
LOG.info(String.format("Master key file exists at %s, resetting.", masterKeyFile.getAbsolutePath()));
FileChannel fileChannel = null;
try {
fileChannel = new FileOutputStream(masterKeyFile).getChannel();
fileChannel.truncate(0);
} catch (FileNotFoundException e) {
LOG.error(String.format("Failed to open key file at %s: %s", masterKeyFile.getAbsolutePath(), e.getLocalizedMessage()), e);
} catch (IOException e) {
LOG.error(String.format("Failed to reset key file at %s: %s", masterKeyFile.getAbsolutePath(), e.getLocalizedMessage()), e);
} finally {
if (fileChannel != null) {
try {
fileChannel.close();
} catch (IOException e) {
// Ignore...
}
}
}
} else {
LOG.info(String.format("File exists at %s, but may not be a master key file. " +
"It must be manually removed before this file location can be used", masterKeyFile.getAbsolutePath()));
return false;
}
}
if (atom != null) {
try {
ArrayList<String> lines = new ArrayList<>();
lines.add(MASTER_PERSISTENCE_TAG_PREFIX + TimeStamp.getCurrentTime().toDateString());
String line = Base64.encodeBase64String((
Base64.encodeBase64String(atom.salt) + "::" +
Base64.encodeBase64String(atom.iv) + "::" +
Base64.encodeBase64String(atom.cipher)).getBytes("UTF8"));
lines.add(line);
FileUtils.writeLines(masterKeyFile, "UTF8", lines);
// restrict os permissions to only the user running this process
protectAccess(masterKeyFile);
} catch (IOException e) {
LOG.error(String.format("Failed to persist master key to %s: %s ", masterKeyFile.getAbsolutePath(), e.getLocalizedMessage()), e);
return false;
}
}
return true;
}
/**
* Determines if the specified file is a "master key" file by checking the file header to see if it
* matches an expected value.
* <p/>
* The "master key" file is expected to have a header (or first line) that starts with "#1.0#". If it,
* it is assumed to be a "master key" file, otherwise it is assumed to not be.
*
* @param file the file to test
* @return true if the file is identitified as "master key" file; otherwise false
*/
private static boolean isMasterKeyFile(File file) {
FileReader reader = null;
try {
reader = new FileReader(file);
char[] buffer = new char[MASTER_PERSISTENCE_TAG_PREFIX.length()];
return (reader.read(buffer) == buffer.length) && Arrays.equals(buffer, MASTER_PERSISTENCE_TAG_PREFIX.toCharArray());
} catch (Exception e) {
// Ignore, assume the file is not a master key file...
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// Ignore...
}
}
}
return false;
}
/**
* Ensures that the owner of this process is the only local user account able to read and write to
* the specified file or read, write to, and execute the specified directory.
*
* @param file the file or directory for which to modify access
*/
private static void protectAccess(File file) throws AmbariException {
if (file.exists()) {
if (!file.setReadable(false, false) || !file.setReadable(true, true)) {
String message = String.format("Failed to set %s readable only by current user", file.getAbsolutePath());
LOG.warn(message);
throw new AmbariException(message);
}
if (!file.setWritable(false, false) || !file.setWritable(true, true)) {
String message = String.format("Failed to set %s writable only by current user", file.getAbsolutePath());
LOG.warn(message);
throw new AmbariException(message);
}
if (file.isDirectory()) {
if (!file.setExecutable(false, false) || !file.setExecutable(true, true)) {
String message = String.format("Failed to set %s executable by current user", file.getAbsolutePath());
LOG.warn(message);
throw new AmbariException(message);
}
} else {
if (!file.setExecutable(false, false)) {
String message = String.format("Failed to set %s not executable", file.getAbsolutePath());
LOG.warn(message);
throw new AmbariException(message);
}
}
}
}
private String readMasterKey() {
String key = null;
Map<String, String> envVariables = System.getenv();
if (envVariables != null && !envVariables.isEmpty()) {
key = envVariables.get(Configuration.MASTER_KEY_ENV_PROP);
if (key == null || key.isEmpty()) {
String keyPath = envVariables.get(Configuration.MASTER_KEY_LOCATION.getKey());
if (keyPath != null && !keyPath.isEmpty()) {
File keyFile = new File(keyPath);
if (keyFile.exists()) {
try {
initializeFromFile(keyFile);
if (master != null) {
key = new String(master);
}
FileUtils.deleteQuietly(keyFile);
} catch (IOException e) {
LOG.error("Cannot read master key from file: " + keyPath);
e.printStackTrace();
} catch (Exception e) {
LOG.error("Cannot read master key from file: " + keyPath);
e.printStackTrace();
}
}
}
}
}
return key;
}
private void initializeFromFile(File masterFile) throws Exception {
try {
List<String> lines = FileUtils.readLines(masterFile, "UTF8");
String tag = lines.get(0);
LOG.info("Loading from persistent master: " + tag);
String line = new String(Base64.decodeBase64(lines.get(1)));
String[] parts = line.split("::");
master = new String(aes.decrypt(Base64.decodeBase64(parts[0]),
Base64.decodeBase64(parts[1]), Base64.decodeBase64(parts[2])),
"UTF8").toCharArray();
} catch (IOException e) {
e.printStackTrace();
throw e;
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}