/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.wildfly.test.security;
import java.io.File;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import org.jboss.security.plugins.PBEUtils;
import org.jboss.security.vault.SecurityVault;
import org.jboss.security.vault.SecurityVaultException;
import org.jboss.security.vault.SecurityVaultFactory;
import org.picketbox.plugins.vault.PicketBoxSecurityVault;
/**
* Non-interactive session for {@link VaultTool}
*
* @author Peter Skopek
*
*/
public final class VaultSession {
public static final String VAULT_ENC_ALGORITHM = "PBEwithMD5andDES";
static final Charset CHARSET = StandardCharsets.UTF_8;
private String keystoreURL;
private String keystorePassword;
private String keystoreMaskedPassword;
private String encryptionDirectory;
private String salt;
private int iterationCount;
private SecurityVault vault;
private String vaultAlias;
/**
* Constructor to create VaultSession.
*
* @param keystoreURL
* @param keystorePassword
* @param encryptionDirectory
* @param salt
* @param iterationCount
* @throws Exception
*/
public VaultSession(String keystoreURL, String keystorePassword, String encryptionDirectory, String salt, int iterationCount)
throws Exception {
this.keystoreURL = keystoreURL;
this.keystorePassword = keystorePassword;
this.encryptionDirectory = encryptionDirectory;
this.salt = salt;
this.iterationCount = iterationCount;
validate();
}
/**
* Validate fields sent to this class's constructor.
*/
private void validate() throws Exception {
validateKeystoreURL();
validateEncryptionDirectory();
validateSalt();
validateIterationCount();
validateKeystorePassword();
}
protected void validateKeystoreURL() throws Exception {
File f = new File(keystoreURL);
if (!f.exists()) {
throw new Exception(String.format("Keystore '%s' doesn't exist." + "\nkeystore could be created: "
+ "keytool -genseckey -alias Vault -storetype jceks -keyalg AES -keysize 128 -storepass secretsecret -keypass secretsecret -keystore %s",
keystoreURL, keystoreURL));
} else if (!f.canWrite() || !f.isFile()) {
throw new Exception(String.format("Keystore [%s] is not writable or not a file.", keystoreURL));
}
}
protected void validateKeystorePassword() throws Exception {
if (keystorePassword == null) {
throw new Exception("Keystore password has to be specified.");
}
}
protected void validateEncryptionDirectory() throws Exception {
if (encryptionDirectory == null) {
throw new Exception("Encryption directory has to be specified.");
}
if (!encryptionDirectory.endsWith("/") || encryptionDirectory.endsWith("\\")) {
encryptionDirectory = encryptionDirectory + (System.getProperty("file.separator", "/"));
}
File d = new File(encryptionDirectory);
if (!d.exists()) {
if (!d.mkdirs()) {
throw new Exception(String.format("Cannot create encryption directory %s", d.getAbsolutePath()));
}
}
if (!d.isDirectory()) {
throw new Exception(String.format("Encryption directory is not a directory or doesn't exist. (%s)", encryptionDirectory));
}
}
protected void validateIterationCount() throws Exception {
if (iterationCount < 1 && iterationCount > Integer.MAX_VALUE) {
throw new Exception(String.format("Iteration count has to be within 1 - "
+ Integer.MAX_VALUE + ", but it is %s.", String.valueOf(iterationCount)));
}
}
protected void validateSalt() throws Exception {
if (salt == null || salt.length() != 8) {
throw new Exception("Salt has to be exactly 8 characters long.");
}
}
/**
* Method to compute masked password based on class attributes.
*
* @return masked password prefixed with {link @PicketBoxSecurityVault.PASS_MASK_PREFIX}.
* @throws Exception
*/
private String computeMaskedPassword() throws Exception {
// Create the PBE secret key
SecretKeyFactory factory = SecretKeyFactory.getInstance(VAULT_ENC_ALGORITHM);
char[] password = "somearbitrarycrazystringthatdoesnotmatter".toCharArray();
PBEParameterSpec cipherSpec = new PBEParameterSpec(salt.getBytes(StandardCharsets.UTF_8), iterationCount);
PBEKeySpec keySpec = new PBEKeySpec(password);
SecretKey cipherKey = factory.generateSecret(keySpec);
String maskedPass = PBEUtils.encode64(keystorePassword.getBytes(StandardCharsets.UTF_8), VAULT_ENC_ALGORITHM, cipherKey, cipherSpec);
return PicketBoxSecurityVault.PASS_MASK_PREFIX + maskedPass;
}
/**
* Initialize the underlying vault.
*
* @throws Exception
*/
private void initSecurityVault() throws Exception {
try {
this.vault = SecurityVaultFactory.get();
this.vault.init(getVaultOptionsMap());
handshake();
} catch (SecurityVaultException e) {
throw new Exception("Exception encountered:", e);
}
}
/**
* Start the vault with given alias.
*
* @param vaultAlias
* @throws Exception
*/
public void startVaultSession(String vaultAlias) throws Exception {
if (vaultAlias == null) {
throw new Exception("Vault alias has to be specified.");
}
this.keystoreMaskedPassword = (org.jboss.security.Util.isPasswordCommand(keystorePassword))
? keystorePassword
: computeMaskedPassword();
this.vaultAlias = vaultAlias;
initSecurityVault();
}
private Map<String, Object> getVaultOptionsMap() {
Map<String, Object> options = new HashMap<String, Object>();
options.put(PicketBoxSecurityVault.KEYSTORE_URL, keystoreURL);
options.put(PicketBoxSecurityVault.KEYSTORE_PASSWORD, keystoreMaskedPassword);
options.put(PicketBoxSecurityVault.KEYSTORE_ALIAS, vaultAlias);
options.put(PicketBoxSecurityVault.SALT, salt);
options.put(PicketBoxSecurityVault.ITERATION_COUNT, Integer.toString(iterationCount));
options.put(PicketBoxSecurityVault.ENC_FILE_DIR, encryptionDirectory);
return options;
}
private void handshake() throws SecurityVaultException {
Map<String, Object> handshakeOptions = new HashMap<String, Object>();
handshakeOptions.put(PicketBoxSecurityVault.PUBLIC_CERT, vaultAlias);
vault.handshake(handshakeOptions);
}
/**
* Add secured attribute to specified vault block. This method can be called only after successful
* startVaultSession() call.
*
* @param vaultBlock
* @param attributeName
* @param attributeValue
* @return secured attribute configuration
*/
public String addSecuredAttribute(String vaultBlock, String attributeName, char[] attributeValue) throws Exception {
vault.store(vaultBlock, attributeName, attributeValue, null);
return securedAttributeConfigurationString(vaultBlock, attributeName);
}
/**
* Add secured attribute to specified vault block. This method can be called only after successful
* startVaultSession() call.
* After successful storage the secured attribute information will be displayed at standard output.
* For silent method @see addSecuredAttribute
*
* @param vaultBlock
* @param attributeName
* @param attributeValue
* @throws Exception
*/
public void addSecuredAttributeWithDisplay(String vaultBlock, String attributeName, char[] attributeValue) throws Exception {
vault.store(vaultBlock, attributeName, attributeValue, null);
attributeCreatedDisplay(vaultBlock, attributeName);
}
/**
* Check whether secured attribute is already set for given vault block and attribute name. This method can be called only after
* successful startVaultSession() call.
*
* @param vaultBlock
* @param attributeName
* @return true is password already exists for given vault block and attribute name.
* @throws Exception
*/
public boolean checkSecuredAttribute(String vaultBlock, String attributeName) throws Exception {
return vault.exists(vaultBlock, attributeName);
}
/**
* This method removes secured attribute stored in {@link SecurityVault}.
* After successful remove operation returns true. Otherwise false.
*
* @param vaultBlock security vault block
* @param attributeName Attribute name stored in security vault
* @return true is operation is successful, otherwise false
* @throws Exception
*/
public boolean removeSecuredAttribute(String vaultBlock, String attributeName) throws Exception {
return vault.remove(vaultBlock, attributeName, null);
}
/**
* Display info about stored secured attribute.
*
* @param vaultBlock
* @param attributeName
*/
private void attributeCreatedDisplay(String vaultBlock, String attributeName) {
System.out.println(String.format("Secured attribute value has been stored in Vault.\n" +
"Please make note of the following:\n" +
"********************************************\n" +
"Vault Block:%s\n" + "Attribute Name:%s\n" +
"Configuration should be done as follows:\n" +
"%s\n" +
"********************************************", vaultBlock, attributeName, securedAttributeConfigurationString(vaultBlock, attributeName)));
}
/**
* Returns configuration string for secured attribute.
*
* @param vaultBlock
* @param attributeName
* @return
*/
private String securedAttributeConfigurationString(String vaultBlock, String attributeName) {
return "VAULT::" + vaultBlock + "::" + attributeName + "::1";
}
/**
* Display info about vault itself in form of AS7 configuration file.
*/
public void vaultConfigurationDisplay() {
System.out.println("Vault Configuration in WildFly configuration file:");
System.out.println("********************************************");
System.out.println("...");
System.out.println("</extensions>");
System.out.print(vaultConfiguration());
System.out.println("<management> ...");
System.out.println("********************************************");
}
/**
* Returns vault configuration string in user readable form.
* @return
*/
public String vaultConfiguration() {
StringBuilder sb = new StringBuilder();
sb.append("<vault>").append("\n");
sb.append(" <vault-option name=\"KEYSTORE_URL\" value=\"").append(keystoreURL).append("\"/>").append("\n");
sb.append(" <vault-option name=\"KEYSTORE_PASSWORD\" value=\"").append(keystoreMaskedPassword).append("\"/>").append("\n");
sb.append(" <vault-option name=\"KEYSTORE_ALIAS\" value=\"").append(vaultAlias).append("\"/>").append("\n");
sb.append(" <vault-option name=\"SALT\" value=\"").append(salt).append("\"/>").append("\n");
sb.append(" <vault-option name=\"ITERATION_COUNT\" value=\"").append(iterationCount).append("\"/>").append("\n");
sb.append(" <vault-option name=\"ENC_FILE_DIR\" value=\"").append(encryptionDirectory).append("\"/>").append("\n");
sb.append("</vault>");
return sb.toString();
}
/**
* Method to get keystore masked password to use further in configuration.
* Has to be used after {@link startVaultSession} method.
*
* @return the keystoreMaskedPassword
*/
public String getKeystoreMaskedPassword() {
return keystoreMaskedPassword;
}
/**
* Display format for couple of vault block and attribute name.
*
* @param vaultBlock
* @param attributeName
* @return formatted {@link String}
*/
static String blockAttributeDisplayFormat(String vaultBlock, String attributeName) {
return "[" + vaultBlock + "::" + attributeName + "]";
}
}