blob: 0aba570b3f099e83e8b86ff3d4e83d82025de649 [file] [log] [blame]
/*
* 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.knox.gateway.services.security.impl;
import de.thetaphi.forbiddenapis.SuppressForbidden;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.net.ntp.TimeStamp;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.i18n.GatewaySpiMessages;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.services.ServiceLifecycleException;
import org.apache.knox.gateway.services.security.EncryptionResult;
import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CMFMasterService {
private static final GatewaySpiMessages LOG = MessagesFactory.get( GatewaySpiMessages.class );
private static final String MASTER_PASSPHRASE = "masterpassphrase";
private static final String MASTER_PERSISTENCE_TAG = "#1.0# " + TimeStamp.getCurrentTime().toDateString();
protected char[] master;
protected String serviceName;
private ConfigurableEncryptor encryptor = new ConfigurableEncryptor(MASTER_PASSPHRASE);
public CMFMasterService(String serviceName) {
super();
this.serviceName = serviceName;
}
public char[] getMasterSecret() {
return this.master;
}
public void setupMasterSecret(String securityDir, String filename,
boolean persisting, GatewayConfig config)
throws ServiceLifecycleException {
encryptor.init(config);
setupMasterSecret(securityDir, filename, persisting);
}
protected void setupMasterSecret(String securityDir, boolean persisting) throws ServiceLifecycleException {
setupMasterSecret(securityDir, serviceName + "-master", persisting);
}
protected void setupMasterSecret(String securityDir, String filename, boolean persisting) throws ServiceLifecycleException {
File masterFile = new File(securityDir, filename);
if (masterFile.exists()) {
try {
initializeFromMaster(masterFile);
} catch (Exception e) {
throw new ServiceLifecycleException("Unable to load the persisted master secret.", e);
}
}
else {
if (master == null) {
displayWarning(persisting);
promptUser();
}
if(persisting) {
persistMaster(master, masterFile);
}
}
}
protected void promptUser() {
Console c = System.console();
if (c == null) {
LOG.unableToPromptForMasterUseKnoxCLI();
System.err.println("No console.");
System.exit(1);
}
boolean valid = false;
do {
char [] newPassword1 = c.readPassword("Enter master secret: ");
char [] newPassword2 = c.readPassword("Enter master secret again: ");
if ( newPassword1.length == 0 ) {
c.format("Password too short. Try again.%n");
} else if (!Arrays.equals(newPassword1, newPassword2) ) {
c.format("Passwords don't match. Try again.%n");
} else {
this.master = Arrays.copyOf(newPassword1, newPassword1.length);
valid = true;
}
Arrays.fill(newPassword1, ' ');
Arrays.fill(newPassword2, ' ');
} while (!valid);
}
protected void displayWarning(boolean persisting) {
Console c = System.console();
if (c == null) {
LOG.unableToPromptForMasterUseKnoxCLI();
System.err.println("No console.");
System.exit(1);
}
if (persisting) {
c.printf("***************************************************************************************************\n");
c.printf("You have indicated that you would like to persist the master secret for this service instance.\n");
c.printf("Be aware that this is less secure than manually entering the secret on startup.\n");
c.printf("The persisted file will be encrypted and primarily protected through OS permissions.\n");
c.printf("***************************************************************************************************\n");
}
else {
c.printf("***************************************************************************************************\n");
c.printf("Be aware that you will need to enter your master secret for future starts exactly as you do here.\n");
c.printf("This secret is needed to access protected resources for the service process.\n");
c.printf("The master secret must be protected, kept secret and not stored in clear text anywhere.\n");
c.printf("***************************************************************************************************\n");
}
}
protected void persistMaster(char[] master, File masterFile) {
try {
EncryptionResult atom = encryptMaster(master);
ArrayList<String> lines = new ArrayList<>();
lines.add(MASTER_PERSISTENCE_TAG);
String line = Base64.encodeBase64String((
Base64.encodeBase64String(atom.salt) + "::" +
Base64.encodeBase64String(atom.iv) + "::" +
Base64.encodeBase64String(atom.cipher)).getBytes(StandardCharsets.UTF_8));
lines.add(line);
FileUtils.writeLines(masterFile, StandardCharsets.UTF_8.name(), lines);
// restrict os permissions to only the user running this process
chmod("600", masterFile);
} catch (IOException e) {
LOG.failedToPersistMasterSecret(e);
}
}
private EncryptionResult encryptMaster(char[] master) throws IOException {
try {
return encryptor.encrypt(new String(master));
} catch (Exception e) {
LOG.failedToEncryptMasterSecret(e);
throw new IOException(e);
}
}
protected void initializeFromMaster(File masterFile) throws Exception {
try {
List<String> lines = FileUtils.readLines(masterFile, StandardCharsets.UTF_8);
String tag = lines.get(0);
LOG.loadingFromPersistentMaster( tag );
String line = new String(Base64.decodeBase64(lines.get(1)), StandardCharsets.UTF_8);
String[] parts = line.split("::");
this.master = new String(encryptor.decrypt(Base64.decodeBase64(parts[0]),
Base64.decodeBase64(parts[1]), Base64.decodeBase64(parts[2])),
StandardCharsets.UTF_8).toCharArray();
} catch (Exception e) {
LOG.failedToInitializeFromPersistentMaster(masterFile.getName(), e);
throw e;
}
}
@SuppressForbidden
private void chmod(String args, File file) throws IOException {
// TODO: move to Java 7 NIO support to add windows as well
// TODO: look into the following for Windows: Runtime.getRuntime().exec("attrib -r myFile");
if (isUnixEnv()) {
//args and file should never be null.
if (args == null || file == null) {
throw new IllegalArgumentException("nullArg");
}
if (!file.exists()) {
throw new IOException("fileNotFound");
}
// " +" regular expression for 1 or more spaces
final String[] argsString = args.split(" +");
List<String> cmdList = new ArrayList<>();
cmdList.add("/bin/chmod");
cmdList.addAll(Arrays.asList(argsString));
cmdList.add(file.getAbsolutePath());
new ProcessBuilder(cmdList).start();
}
}
private boolean isUnixEnv() {
return (File.separatorChar == '/');
}
}