diff --git a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/FileSystemRealmUtil.java b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/FileSystemRealmUtil.java
index 38070c54629..f4899cb1a88 100644
--- a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/FileSystemRealmUtil.java
+++ b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/FileSystemRealmUtil.java
@@ -19,6 +19,7 @@
import java.util.List;
+
import org.wildfly.common.Assert;
import org.wildfly.security.auth.principal.NamePrincipal;
import org.wildfly.security.auth.server.ModifiableRealmIdentity;
@@ -31,28 +32,29 @@
* A utility class to utilize methods from the {@code FileSystemSecurityRealm} class for the Elytron Tool.
*
* @author Ashpan Raskar
+ * @author Cameron Rodriguez
*/
public class FileSystemRealmUtil {
/**
- * Converts a pre-existing unencrypted {@code FileSystemSecurityRealm} to a newly created encrypted {@code FileSystemSecurityRealm}
+ * Copies identities from an existing {@code FileSystemSecurityRealm} to a new one.
*
- * @param unencryptedRealm the {@code FileSystemSecurityRealm} without any encryption applied
- * @param encryptedRealm the {@code FileSystemSecurityRealm} configured with a SecretKey to encrypt identity data
- * @throws RealmUnavailableException if either realm is unavailable
+ * @param oldRealm the existing {@code FileSystemSecurityRealm} with the identities
+ * @param newRealm the new {@code FileSystemSecurityRealm}
+ * @throws RealmUnavailableException if either realm is unavailable or an operation fails
*/
- public static void createEncryptedRealmFromUnencrypted(FileSystemSecurityRealm unencryptedRealm, FileSystemSecurityRealm encryptedRealm) throws RealmUnavailableException {
- Assert.checkNotNullParam("unencryptedRealm", unencryptedRealm);
- Assert.checkNotNullParam("encryptedRealm", encryptedRealm);
+ public static void cloneIdentitiesToNewRealm(FileSystemSecurityRealm oldRealm, FileSystemSecurityRealm newRealm) throws RealmUnavailableException {
+ Assert.checkNotNullParam("Old FileSystem Realm", oldRealm);
+ Assert.checkNotNullParam("New FileSystem Realm", newRealm);
- ModifiableRealmIdentityIterator realmIterator = unencryptedRealm.getRealmIdentityIterator();
+ ModifiableRealmIdentityIterator realmIterator = oldRealm.getRealmIdentityIterator();
while (realmIterator.hasNext()) {
- ModifiableRealmIdentity identity = realmIterator.next();
- List credentials = ((FileSystemSecurityRealm.Identity) identity).loadCredentials();
- Attributes attributes = identity.getAttributes();
+ ModifiableRealmIdentity oldIdentity = realmIterator.next();
+ List credentials = ((FileSystemSecurityRealm.Identity) oldIdentity).loadCredentials();
+ Attributes attributes = oldIdentity.getAttributes();
- ModifiableRealmIdentity newIdentity = encryptedRealm.getRealmIdentityForUpdate(new NamePrincipal(identity.getRealmIdentityPrincipal().getName()));
+ ModifiableRealmIdentity newIdentity = newRealm.getRealmIdentityForUpdate(new NamePrincipal(oldIdentity.getRealmIdentityPrincipal().getName()));
newIdentity.create();
newIdentity.setCredentials(credentials);
newIdentity.setAttributes(attributes);
@@ -60,5 +62,4 @@ public static void createEncryptedRealmFromUnencrypted(FileSystemSecurityRealm u
}
realmIterator.close();
}
-
}
diff --git a/tool/src/main/java/org/wildfly/security/tool/Command.java b/tool/src/main/java/org/wildfly/security/tool/Command.java
index 0bcc04e110b..44e212d70fe 100644
--- a/tool/src/main/java/org/wildfly/security/tool/Command.java
+++ b/tool/src/main/java/org/wildfly/security/tool/Command.java
@@ -23,13 +23,23 @@
import java.io.BufferedReader;
import java.io.Console;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOError;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -41,6 +51,7 @@
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Supplier;
+import java.util.regex.Pattern;
import javax.crypto.SecretKey;
@@ -50,7 +61,11 @@
import org.wildfly.security.credential.store.UnsupportedCredentialTypeException;
import org.wildfly.security.credential.store.impl.PropertiesCredentialStore;
import org.wildfly.security.encryption.SecretKeyUtil;
+import org.wildfly.security.keystore.AtomicLoadKeyStore;
+import org.wildfly.security.keystore.KeyStoreUtil;
+import org.wildfly.security.keystore.WildFlyElytronKeyStoreProvider;
import org.wildfly.security.password.WildFlyElytronPasswordProvider;
+import org.wildfly.security.provider.util.ProviderUtil;
/**
* Base command class
@@ -67,7 +82,8 @@ public abstract class Command {
public static final int INPUT_DATA_NOT_CONFIRMED = 3;
- public static Supplier ELYTRON_PASSWORD_PROVIDERS = () -> new Provider[] {
+ public static Supplier ELYTRON_KS_PASS_PROVIDERS = () -> new Provider[] {
+ WildFlyElytronKeyStoreProvider.getInstance(),
WildFlyElytronPasswordProvider.getInstance()
};
@@ -378,10 +394,79 @@ SecretKey getSecretKey(Boolean createCredentialStore, String credentialStoreLoca
return key;
}
+
+ /**
+ * Acquire a given keypair from a {@link KeyStore}.
+ *
+ * @param keyPairAlias the name for a keypair within the keyStore
+ * @param descriptorBlockCount index of a descriptor block from bulk conversion files
+ * @return the requested {@link KeyPair}, or {@code null} if it could not be retrieved
+ * @throws CertificateException if the KeyStore cannot be loaded
+ * @throws NoSuchAlgorithmException if the algorithm to verify the KeyStore's integrity is not available
+ */
+ KeyPair getKeyPair(Path keyStorePath, String keyStoreType, String keyPairAlias, char[] password,
+ int descriptorBlockCount) throws CertificateException, NoSuchAlgorithmException {
+ AtomicLoadKeyStore keyStore = null;
+ if (keyStoreType != null) {
+ keyStore = AtomicLoadKeyStore.newInstance(
+ keyStoreType,
+ ProviderUtil.findProvider(ProviderUtil.INSTALLED_PROVIDERS, null, KeyStore.class, keyStoreType)
+ );
+ }
+
+ File resolvedPath = null;
+ if (keyStorePath != null && keyStorePath.toFile().exists()) {
+ try {
+ resolvedPath = keyStorePath.toAbsolutePath().toFile();
+ } catch (IOError | SecurityException e) {
+ warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockInvalidKeyStorePath(descriptorBlockCount));
+ return null;
+ }
+ }
+ if (resolvedPath == null){
+ warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockInvalidKeyStorePath(descriptorBlockCount));
+ return null;
+ }
+
+ try (FileInputStream is = new FileInputStream(resolvedPath)) {
+ if (keyStoreType != null) {
+ keyStore.load(is, password);
+ } else {
+ KeyStore detected = KeyStoreUtil.loadKeyStore(ProviderUtil.INSTALLED_PROVIDERS, null,
+ is, resolvedPath.getPath(), password);
+ keyStore = AtomicLoadKeyStore.atomize(detected);
+ }
+ } catch (IOException | KeyStoreException e) {
+ warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockKeyStoreNotLoaded(descriptorBlockCount));
+ return null;
+ }
+
+ PrivateKey privateKey;
+ Certificate publicKeyCert;
+ try {
+ privateKey = (PrivateKey) keyStore.getKey(keyPairAlias, password);
+ publicKeyCert = keyStore.getCertificate(keyPairAlias);
+ } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
+ warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockKeyPairFail(descriptorBlockCount));
+ return null;
+ }
+
+ if (privateKey == null) {
+ warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockMissingPrivateKey(descriptorBlockCount));
+ return null;
+ }
+ if (publicKeyCert == null) {
+ warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockMissingPublicKey(descriptorBlockCount));
+ return null;
+ }
+
+ return new KeyPair(publicKeyCert.getPublicKey(), privateKey);
+ }
}
class Params {
static final String ALIAS_PARAM = "alias";
+ static final String BOOLEAN_PARAM = "true/false";
static final String BULK_CONVERT_PARAM = "bulk-convert";
static final String CREDENTIAL_STORE_LOCATION_PARAM = "credential-store";
static final String CREATE_CREDENTIAL_STORE_PARAM = "create";
@@ -397,12 +482,16 @@ class Params {
static final String IMPLEMENTATION_PROPERTIES_PARAM = "properties";
static final String INPUT_LOCATION_PARAM = "input-location";
static final String ITERATION_PARAM = "iteration";
+ static final String KEY_PAIR_ALIAS_PARAM = "key-pair";
static final String KEYSTORE_PARAM = "keystore";
+ static final String KEYSTORE_TYPE_PARAM = "type";
static final String LEVELS_PARAM = "levels";
static final String NAME_PARAM = "name";
+ static final String NUMBER_PARAM = "number";
static final String OTHER_PROVIDERS_PARAM = "other-providers";
static final String OUTPUT_LOCATION_PARAM = "output-location";
static final String PASSWORD_PARAM = "password";
+ static final String PASSWORD_ENV_PARAM = "password-env";
static final String REALM_NAME_PARAM = "realm-name";
static final String SALT_PARAM = "salt";
static final String SECRET_KEY_ALIAS_PARAM = "secret-key";
@@ -411,6 +500,8 @@ class Params {
static final String SUMMARY_PARAM = "summary";
// Other constants
+ static final Pattern BOOLEAN_ARG_REGEX = Pattern.compile("(true|false)", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
+ static final String DEFAULT_KEY_PAIR_ALIAS = "integrity-key";
static final Integer DEFAULT_LEVELS = 2;
static final String DEFAULT_SECRET_KEY_ALIAS = "key";
static final String FILE_SEPARATOR = File.separator;
diff --git a/tool/src/main/java/org/wildfly/security/tool/ElytronTool.java b/tool/src/main/java/org/wildfly/security/tool/ElytronTool.java
index 10d51f47596..0d0d4193a10 100644
--- a/tool/src/main/java/org/wildfly/security/tool/ElytronTool.java
+++ b/tool/src/main/java/org/wildfly/security/tool/ElytronTool.java
@@ -58,6 +58,7 @@ public ElytronTool() {
commandRegistry.put(VaultCommand.VAULT_COMMAND, new VaultCommand()); // uses exit code 7
commandRegistry.put(FileSystemRealmCommand.FILE_SYSTEM_REALM_COMMAND, new FileSystemRealmCommand()); // uses exit code 7
commandRegistry.put(FileSystemEncryptRealmCommand.FILE_SYSTEM_ENCRYPT_COMMAND, new FileSystemEncryptRealmCommand()); // uses exit code 7
+ commandRegistry.put(FileSystemRealmIntegrityCommand.FILE_SYSTEM_REALM_INTEGRITY_COMMAND, new FileSystemRealmIntegrityCommand()); // uses exit code 7
}
/**
diff --git a/tool/src/main/java/org/wildfly/security/tool/ElytronToolMessages.java b/tool/src/main/java/org/wildfly/security/tool/ElytronToolMessages.java
index d47bf892e30..e842825ff2a 100644
--- a/tool/src/main/java/org/wildfly/security/tool/ElytronToolMessages.java
+++ b/tool/src/main/java/org/wildfly/security/tool/ElytronToolMessages.java
@@ -76,6 +76,12 @@ public interface ElytronToolMessages extends BasicLogger {
@Message(id = NONE, value = "Password for credential store")
String cmdLineCredentialStorePassword();
+ @Message(id = NONE, value = "Password for KeyStore. Can also be provided by console prompt.")
+ String cmdLineKeyStorePassword();
+
+ @Message(id = NONE, value = "Name of an environment variable from which to resolve the KeyStore password.")
+ String cmdLineKeyStorePasswordEnv();
+
@Message(id = NONE, value = "Salt to apply for final masked password of the credential store")
String cmdLineSaltDesc();
@@ -481,6 +487,16 @@ public interface ElytronToolMessages extends BasicLogger {
"Blocks of options must be separated by a blank line.")
String cmdFileSystemRealmEncryptBulkConvertDesc();
+ @Message(id = NONE, value = "Bulk conversion with options listed in description file. (Action)" +
+ "Optional options have defaults and can be skipped ([type, default_or_NULL]), required options do not (). %n" +
+ "One of either password or password-env is required. %n" +
+ "Blocks of options must be separated by a blank line; order is not important. Syntax: %n" +
+ "input-location:%n" + "output-location:[directory,NULL]%n" + "realm-name:[name,filesystem-realm-with-integrity]%n" +
+ "keystore:%n" + "type:[name,NULL]%n" + "password:[password,NULL]%n" + "password-env:[name,NULL]%n" +
+ "key-pair:[name,integrity-key]%n" + "credential-store:[file,NULL]%n" + "secret-key:[name,NULL]%n" +
+ "levels:[number,2]%n" + "hash-encoding:[name,BASE64]%n" + "hash-charset:[name,UTF-8]%n" + "encoded:[bool,true]")
+ String cmdFileSystemRealmIntegrityBulkConvertDesc();
+
// filesystem-realm encrypt command
@Message(id = NONE, value = "'FileSystemRealmEncrypt' command is used to convert un-encrypted FileSystemSecurityRealm(s) to encrypted FileSystemSecurityRealm(s) with a SecretKey.")
String cmdFileSystemEncryptHelpHeader();
@@ -488,9 +504,64 @@ public interface ElytronToolMessages extends BasicLogger {
@Message(id = NONE, value = "Secret Key was not found in the Credential Store at %s, and populate option was not set. Skipping descriptor file block number %d.")
String cmdFileSystemEncryptionNoSecretKey(String credentialStorePath, Integer blockNumber);
+ @Message(id = NONE, value = "The directory where the new filesystem realm resides. If not provided, realm will be upgraded in-place (with backup), %n" +
+ "and realm-name option will not be used in file path.")
+ String cmdFileSystemRealmIntegrityOutputLocationDesc();
+
+ @Message(id = NONE, value = "The name of the new filesystem-realm. Will be appended to output-location path (if output-location is provided). %n" +
+ "When not set, nothing is appended to the path, and `filesystem-realm-with-integrity` is used for the WildFly resource name.%n")
+ String cmdFileSystemRealmIntegrityNewRealmDesc();
+
+ @Message(id = NONE, value = "The relative or absolute path to the KeyStore file that contains the key pair.")
+ String cmdFileSystemRealmIntegrityKeyStoreDesc();
+
+ @Message(id = NONE, value = "The type of KeyStore to be used. Optional.")
+ String cmdFileSystemRealmIntegrityKeyStoreTypeDesc();
+
+ @Message(id = NONE, value = "The alias of the key pair to be used, within the KeyStore. Set to integrity-key by default.")
+ String cmdFileSystemRealmIntegrityKeyPairAliasDesc();
+
+ @Message(id = NONE, value = "The relative or absolute path to the secret-key-credential-store file. Only %n" +
+ "applicable if the filesystem realm is encrypted.")
+ String cmdFileSystemRealmIntegrityCredentialStoreDesc();
+
+ @Message(id = NONE, value = "The alias of the secret key stored in the credential store file. Set to key by default, only %n" +
+ "applicable if the filesystem realm is encrypted.")
+ String cmdFileSystemRealmIntegritySecretKeyDesc();
+
+ @Message(id = NONE, value = "The number of levels used in the input filesystem realm. Set to 2 by default.")
+ String cmdFileSystemRealmIntegrityLevelsDesc();
+
+ @Message(id = NONE, value = "The hash encoding used in the input filesystem realm. Set to BASE64 by default.%n" +
+ "Regardless of setting, the output realm will always be BASE64-encoded.")
+ String cmdFileSystemRealmIntegrityHashEncodingDesc();
+
@Message(id = NONE, value = "The character set used to convert the password string to a byte array. Defaults to UTF-8.")
String cmdFileSystemRealmIntegrityHashCharsetDesc();
+ @Message(id = NONE, value = "Indicates if the original realm used Base32-encoded identities as file names.%n" +
+ "Set to true by default. Regardless of setting, the output realm will always use Base32-encoding in file names.")
+ String cmdFileSystemRealmIntegrityEncodedDesc();
+
+ @Message(id = NONE, value = "Both --bulk-convert and at least one other realm configuration option was specified. %n" +
+ "The bulk-convert option may only be used with --help, --debug, --silent, and --summary options.")
+ MissingOptionException mutuallyExclusiveOptionsIntegritySpecified();
+
+ @Message(id = NONE, value = "'FileSystem Realm Integrity' command is used to sign existing, non-empty FileSystem Security Realms with a key pair, for future integrity validation.")
+ String cmdFileSystemIntegrityHelpHeader();
+
+ @Message(id = NONE, value = "KeyStore password: ")
+ String keyStorePasswordPrompt();
+
+ @Message(id = NONE, value = "KeyStore path not specified.")
+ MissingArgumentException keyStorePathNotSpecified();
+
+ @Message(id = NONE, value = "KeyStore does not exist.")
+ MissingArgumentException keyStoreDoesNotExist();
+
+ @Message(id = NONE, value = "Encoded option must be set to 'true' or 'false'.")
+ IllegalArgumentException encodedMustBeBoolean();
+
@Message(id = NONE, value = "Suppresses all output except errors and prompts.")
String cmdFileSystemRealmSilentDesc();
@@ -518,6 +589,10 @@ public interface ElytronToolMessages extends BasicLogger {
@Message(id = NONE, value = "Could not find the specified file %s.")
FileNotFoundException fileNotFound(String file);
+ @Message(id = NONE, value = "Could not copy input filesystem realm at %s for in-place upgrade.%n" +
+ "Output filesystem will be placed at %s")
+ String unableToUpgradeInPlace(String inputPath, String newOutputPath);
+
@Message(id = NONE, value = "Skipping descriptor file block number %d due to %s.")
String skippingDescriptorBlock(Integer blockNumber, String reason);
@@ -531,12 +606,44 @@ public interface ElytronToolMessages extends BasicLogger {
@Message(id = NONE, value = "Skipping descriptor file block number %d due to missing output realm location.")
String skippingDescriptorBlockOutputLocation(Integer blockNumber);
+ @Message(id = NONE, value = "Skipping descriptor file block number %d due to missing KeyStore path.")
+ String skippingDescriptorBlockKeyStorePath(Integer blockName);
+
+ @Message(id = NONE, value = "Skipping descriptor file block number %d due to missing KeyStore password.")
+ String skippingDescriptorBlockPassword(Integer blockName);
+
+ @Message(id = NONE, value = "Skipping descriptor file block number %d due to invalid KeyStore path.")
+ String skippingDescriptorBlockInvalidKeyStorePath(Integer blockNumber);
+
+ @Message(id = NONE, value = "Skipping descriptor file block number %d due to failure to load KeyStore.")
+ String skippingDescriptorBlockKeyStoreNotLoaded(Integer blockNumber);
+
+ @Message(id = NONE, value = "Skipping descriptor file block number %d due to failure to load key pair.")
+ String skippingDescriptorBlockKeyPairFail(Integer blockNumber);
+
+ @Message(id = NONE, value = "Skipping descriptor file block number %d due to missing private key.")
+ String skippingDescriptorBlockMissingPrivateKey(Integer blockNumber);
+
+ @Message(id = NONE, value = "Skipping descriptor file block number %d due to missing public key.")
+ String skippingDescriptorBlockMissingPublicKey(Integer blockNumber);
+
@Message(id = NONE, value = "Skipping descriptor file block number %d due to missing new filesystem realm name.")
String skippingDescriptorBlockFilesystemRealmName(Integer blockNumber);
+ @Message(id = NONE, value = "Skipping descriptor file block number %d due to no identities present in filesystem realm.%n" +
+ "Keys for this realm can be added via the management client.")
+ String skippingDescriptorBlockEmptyRealm(Integer blockNumber);
+
@Message(id = NONE, value = "Creating encrypted realm for: %s")
String fileSystemRealmEncryptCreatingRealm(String realmName);
+ @Message(id = NONE, value = "Creating filesystem realm with integrity verification for: %s")
+ String fileSystemRealmIntegrityCreatingRealm(String realmName);
+
+ @Message(id = NONE, value = "In-place upgrade for descriptor block %d: filesystem realm backed up at %s%n" +
+ "Realm name will not be used in output realm path.")
+ String fileSystemRealmIntegrityInPlaceBackup(Integer blockNumber, String backupLocation);
+
@Message(id = NONE, value = "Should file %s be overwritten? (y/n) ")
String shouldFileBeOverwritten(String file);
diff --git a/tool/src/main/java/org/wildfly/security/tool/FileSystemEncryptRealmCommand.java b/tool/src/main/java/org/wildfly/security/tool/FileSystemEncryptRealmCommand.java
index 4b0796c6189..b77582e17bf 100644
--- a/tool/src/main/java/org/wildfly/security/tool/FileSystemEncryptRealmCommand.java
+++ b/tool/src/main/java/org/wildfly/security/tool/FileSystemEncryptRealmCommand.java
@@ -707,17 +707,17 @@ private void createFileSystemRealm() throws Exception {
.setHashEncoding(descriptor.getHashEncoding())
.setHashCharset(descriptor.getHashCharset())
.setEncoded(descriptor.getEncoded())
- .setProviders(ELYTRON_PASSWORD_PROVIDERS)
+ .setProviders(ELYTRON_KS_PASS_PROVIDERS)
.build();
FileSystemSecurityRealm newFileSystemRealm = FileSystemSecurityRealm.builder()
.setRoot(Paths.get(descriptor.getOutputRealmLocation(), descriptor.getFileSystemRealmName()))
.setSecretKey(key)
.setLevels(descriptor.getLevels())
- .setProviders(ELYTRON_PASSWORD_PROVIDERS)
+ .setProviders(ELYTRON_KS_PASS_PROVIDERS)
.setHashCharset(descriptor.getHashCharset())
.build();
- FileSystemRealmUtil.createEncryptedRealmFromUnencrypted(oldFileSystemRealm, newFileSystemRealm);
+ FileSystemRealmUtil.cloneIdentitiesToNewRealm(oldFileSystemRealm, newFileSystemRealm);
}
}
diff --git a/tool/src/main/java/org/wildfly/security/tool/FileSystemRealmIntegrityCommand.java b/tool/src/main/java/org/wildfly/security/tool/FileSystemRealmIntegrityCommand.java
new file mode 100644
index 00000000000..3baa32da26d
--- /dev/null
+++ b/tool/src/main/java/org/wildfly/security/tool/FileSystemRealmIntegrityCommand.java
@@ -0,0 +1,1073 @@
+/*
+ * Copyright 2023 Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.wildfly.security.tool;
+
+import static org.wildfly.security.tool.ElytronTool.ElytronToolExitStatus_OK;
+import static org.wildfly.security.tool.Params.BOOLEAN_ARG_REGEX;
+import static org.wildfly.security.tool.Params.BOOLEAN_PARAM;
+import static org.wildfly.security.tool.Params.BULK_CONVERT_PARAM;
+import static org.wildfly.security.tool.Params.CREDENTIAL_STORE_LOCATION_PARAM;
+import static org.wildfly.security.tool.Params.DEBUG_PARAM;
+import static org.wildfly.security.tool.Params.DEFAULT_KEY_PAIR_ALIAS;
+import static org.wildfly.security.tool.Params.DEFAULT_LEVELS;
+import static org.wildfly.security.tool.Params.DIRECTORY_PARAM;
+import static org.wildfly.security.tool.Params.ENCODED_PARAM;
+import static org.wildfly.security.tool.Params.FILE_PARAM;
+import static org.wildfly.security.tool.Params.FILE_SEPARATOR;
+import static org.wildfly.security.tool.Params.HASH_CHARSET_PARAM;
+import static org.wildfly.security.tool.Params.HASH_ENCODING_PARAM;
+import static org.wildfly.security.tool.Params.HELP_PARAM;
+import static org.wildfly.security.tool.Params.INPUT_LOCATION_PARAM;
+import static org.wildfly.security.tool.Params.KEYSTORE_PARAM;
+import static org.wildfly.security.tool.Params.KEYSTORE_TYPE_PARAM;
+import static org.wildfly.security.tool.Params.KEY_PAIR_ALIAS_PARAM;
+import static org.wildfly.security.tool.Params.LEVELS_PARAM;
+import static org.wildfly.security.tool.Params.LINE_SEPARATOR;
+import static org.wildfly.security.tool.Params.NAME_PARAM;
+import static org.wildfly.security.tool.Params.NUMBER_PARAM;
+import static org.wildfly.security.tool.Params.OUTPUT_LOCATION_PARAM;
+import static org.wildfly.security.tool.Params.PASSWORD_ENV_PARAM;
+import static org.wildfly.security.tool.Params.PASSWORD_PARAM;
+import static org.wildfly.security.tool.Params.REALM_NAME_PARAM;
+import static org.wildfly.security.tool.Params.SECRET_KEY_ALIAS_PARAM;
+import static org.wildfly.security.tool.Params.SILENT_PARAM;
+import static org.wildfly.security.tool.Params.SUMMARY_DIVIDER;
+import static org.wildfly.security.tool.Params.SUMMARY_PARAM;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.security.KeyPair;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import javax.crypto.SecretKey;
+
+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.OptionGroup;
+import org.apache.commons.cli.Options;
+import org.wildfly.security.auth.realm.FileSystemRealmUtil;
+import org.wildfly.security.auth.realm.FileSystemSecurityRealm;
+import org.wildfly.security.auth.realm.FileSystemSecurityRealmBuilder;
+import org.wildfly.security.password.spec.Encoding;
+
+/**
+ * Elytron Tool command to enable integrity checking in filesystem realms that previously did not have it enabled. If
+ * any identities use a schema which doesn't support integrity checking ({@code urn:elytron:identity:1.1} or earlier),
+ * they are also updated.
+ *
+ * @author Cameron Rodriguez
+ */
+public class FileSystemRealmIntegrityCommand extends Command {
+ static final String FILE_SYSTEM_REALM_INTEGRITY_COMMAND = "filesystem-realm-integrity";
+
+ static final String DEFAULT_FILESYSTEM_REALM_NAME = "filesystem-realm-with-integrity";
+ static final String MISSING = "MISSING";
+
+ private final List descriptors = new ArrayList<>();
+ static final List PARAMS_LIST = new ArrayList<>(Arrays.asList(INPUT_LOCATION_PARAM, OUTPUT_LOCATION_PARAM,
+ KEYSTORE_PARAM, PASSWORD_PARAM, KEY_PAIR_ALIAS_PARAM, CREDENTIAL_STORE_LOCATION_PARAM, SECRET_KEY_ALIAS_PARAM));
+
+ private final Options options = new Options();
+ private final CommandLineParser parser = new DefaultParser();
+ private boolean silentMode = false;
+ private boolean summaryMode = false;
+ private final StringBuilder summaryString = new StringBuilder();
+ private boolean warningOccurred = false;
+
+ FileSystemRealmIntegrityCommand() {
+ options.addOption(Option.builder("i").longOpt(INPUT_LOCATION_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemEncryptInputLocationDesc())
+ .hasArg().argName(DIRECTORY_PARAM)
+ .build());
+ options.addOption(Option.builder("o").longOpt(OUTPUT_LOCATION_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmIntegrityOutputLocationDesc())
+ .hasArg().argName(DIRECTORY_PARAM)
+ .build());
+ options.addOption(Option.builder("r").longOpt(REALM_NAME_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmIntegrityNewRealmDesc())
+ .hasArg().argName(NAME_PARAM)
+ .build());
+ options.addOption(Option.builder("k").longOpt(KEYSTORE_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmIntegrityKeyStoreDesc())
+ .hasArg().argName(FILE_PARAM)
+ .build());
+ options.addOption(Option.builder("t").longOpt(KEYSTORE_TYPE_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmIntegrityKeyStoreTypeDesc())
+ .hasArg().argName(NAME_PARAM)
+ .build());
+
+ // Password by terminal or environment variable, optional
+ options.addOptionGroup(new OptionGroup()
+ .addOption(Option.builder("p").longOpt(PASSWORD_PARAM).desc(ElytronToolMessages.msg.cmdLineKeyStorePassword())
+ .hasArg().argName(PASSWORD_PARAM)
+ .build())
+ .addOption(Option.builder("pe").longOpt(PASSWORD_ENV_PARAM).desc(ElytronToolMessages.msg.cmdLineKeyStorePasswordEnv())
+ .hasArg().argName(NAME_PARAM)
+ .build()));
+
+ options.addOption(Option.builder("a").longOpt(KEY_PAIR_ALIAS_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmIntegrityKeyPairAliasDesc())
+ .hasArg().argName(NAME_PARAM)
+ .build());
+
+ // Other filesystem realm configuration options
+ options.addOption(Option.builder("c").longOpt(CREDENTIAL_STORE_LOCATION_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmIntegrityCredentialStoreDesc())
+ .hasArg().argName(FILE_PARAM)
+ .build());
+ options.addOption(Option.builder("s").longOpt(SECRET_KEY_ALIAS_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmIntegritySecretKeyDesc())
+ .hasArg().argName(NAME_PARAM)
+ .build());
+ options.addOption(Option.builder("l").longOpt(LEVELS_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmIntegrityLevelsDesc())
+ .hasArg().argName(NUMBER_PARAM)
+ .build());
+ options.addOption(Option.builder("e").longOpt(HASH_ENCODING_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmIntegrityHashEncodingDesc())
+ .hasArg().argName(NAME_PARAM)
+ .build());
+ options.addOption(Option.builder("u").longOpt(HASH_CHARSET_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmIntegrityHashCharsetDesc())
+ .hasArg().argName(NAME_PARAM)
+ .build());
+ options.addOption(Option.builder("f").longOpt(ENCODED_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmIntegrityEncodedDesc())
+ .hasArg().argName(BOOLEAN_PARAM)
+ .build());
+ options.addOption(Option.builder("b").longOpt(BULK_CONVERT_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmIntegrityBulkConvertDesc())
+ .hasArg().argName(FILE_PARAM)
+ .build());
+
+ // General options
+ options.addOption(Option.builder("h").longOpt(HELP_PARAM).desc(ElytronToolMessages.msg.cmdLineHelp())
+ .build());
+ options.addOption(Option.builder("d").longOpt(DEBUG_PARAM).desc(ElytronToolMessages.msg.cmdLineDebug())
+ .build());
+ options.addOption(Option.builder().longOpt(SILENT_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmSilentDesc())
+ .build());
+ options.addOption(Option.builder().longOpt(SUMMARY_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmSummaryDesc())
+ .build());
+ }
+
+ private static final class Descriptor {
+ private Path inputRealmPath;
+ private Path outputRealmPath;
+ private String fileSystemRealmName;
+ private Path keyStorePath;
+ private String keyStoreType;
+ private char[] password;
+ private String passwordEnv;
+ private String keyPairAlias;
+
+ private Path credentialStorePath;
+ private String secretKeyAlias;
+ private Integer levels;
+ private Encoding hashEncoding;
+ private Charset hashCharset;
+ private Boolean encoded;
+
+ private Boolean upgradeInPlace;
+ private Boolean missingRequiredValue;
+ private Boolean realmUpgraded;
+
+ Descriptor() {
+ this.upgradeInPlace = false;
+ this.missingRequiredValue = false;
+ this.realmUpgraded = false;
+ }
+
+ Descriptor(Descriptor descriptor) {
+ this.inputRealmPath = descriptor.inputRealmPath;
+ this.outputRealmPath = descriptor.outputRealmPath;
+ this.fileSystemRealmName = descriptor.fileSystemRealmName;
+ this.keyStorePath = descriptor.keyStorePath;
+ this.keyStoreType = descriptor.keyStoreType;
+ this.password = descriptor.password;
+ this.passwordEnv = descriptor.passwordEnv;
+ this.keyPairAlias = descriptor.keyPairAlias;
+
+ this.credentialStorePath = descriptor.credentialStorePath;
+ this.secretKeyAlias = descriptor.secretKeyAlias;
+ this.levels = descriptor.levels;
+ this.hashEncoding = descriptor.hashEncoding;
+ this.hashCharset = descriptor.hashCharset;
+ this.encoded = descriptor.encoded;
+
+ this.upgradeInPlace = descriptor.upgradeInPlace;
+ this.missingRequiredValue = descriptor.missingRequiredValue;
+ this.realmUpgraded = descriptor.realmUpgraded;
+ }
+
+ /**
+ * Retrieve a value by name, as a string, or null if no value was found.
+ *
+ * @param param the long name of a parameter, like {@code KEYSTORE_PARAM}
+ * */
+ public String getString(String param) {
+ switch (param) {
+ case INPUT_LOCATION_PARAM: // Required param
+ if (inputRealmPath == null) {
+ return null;
+ } else if (inputRealmPath.endsWith(MISSING)) {
+ return MISSING;
+ } else {
+ return inputRealmPath.toString();
+ }
+ case OUTPUT_LOCATION_PARAM:
+ return outputRealmPath != null ? outputRealmPath.toString() : null;
+ case REALM_NAME_PARAM:
+ return fileSystemRealmName;
+ case KEYSTORE_PARAM: // Required param
+ if (keyStorePath == null) {
+ return null;
+ } else if (keyStorePath.endsWith(MISSING)) {
+ return MISSING;
+ } else {
+ return keyStorePath.toString();
+ }
+ case KEYSTORE_TYPE_PARAM:
+ return keyStoreType;
+ case PASSWORD_PARAM: // Required param
+ return password != null ? new String(password) : null;
+ case PASSWORD_ENV_PARAM:
+ return passwordEnv;
+ case KEY_PAIR_ALIAS_PARAM:
+ return keyPairAlias;
+ case CREDENTIAL_STORE_LOCATION_PARAM:
+ return credentialStorePath != null ? credentialStorePath.toString() : null;
+ case SECRET_KEY_ALIAS_PARAM:
+ return secretKeyAlias;
+ case LEVELS_PARAM:
+ return levels != null ? levels.toString() : null;
+ case HASH_ENCODING_PARAM:
+ return hashEncoding != null ? hashEncoding.name() : null;
+ case HASH_CHARSET_PARAM:
+ return hashCharset != null ? hashCharset.name() : null;
+ case ENCODED_PARAM:
+ return encoded != null ? encoded.toString() : null;
+ default:
+ return null;
+ }
+ }
+ public Path getInputRealmPath() {
+ return inputRealmPath;
+ }
+ public Path getOutputRealmPath() {
+ return outputRealmPath;
+ }
+ public String getFileSystemRealmName() {
+ return fileSystemRealmName;
+ }
+ public Path getKeyStorePath() {
+ return keyStorePath;
+ }
+ public String getKeyStoreType() {
+ return keyStoreType;
+ }
+ public char[] getPassword() {
+ return password;
+ }
+ public String getPasswordEnv() {
+ return passwordEnv;
+ }
+ public String getKeyPairAlias() {
+ return keyPairAlias;
+ }
+ public Path getCredentialStorePath() {
+ return credentialStorePath;
+ }
+ public String getSecretKeyAlias() {
+ return secretKeyAlias;
+ }
+ public Integer getLevels() {
+ return levels;
+ }
+ public Encoding getHashEncoding() {
+ return hashEncoding;
+ }
+ public Charset getHashCharset() {
+ return hashCharset;
+ }
+ public Boolean getEncoded() {
+ return encoded;
+ }
+ public Boolean getUpgradeInPlace() {
+ return upgradeInPlace;
+ }
+ public Boolean getMissingRequiredValue() {
+ return missingRequiredValue;
+ }
+
+ /** @return if the filesystem realm was successfully upgraded. Used to determine if a CLI script should be generated. */
+ public Boolean getRealmUpgraded() {
+ return realmUpgraded;
+ }
+
+ public void setInputRealmPath(String inputRealmPath) {
+ setInputRealmPath(Path.of(inputRealmPath).normalize().toAbsolutePath());
+ }
+ public void setInputRealmPath(Path inputRealmPath) {
+ this.inputRealmPath = inputRealmPath.normalize().toAbsolutePath();
+ }
+ public void setOutputRealmPath(String outputRealmPath) {
+ setOutputRealmPath(Path.of(outputRealmPath).normalize().toAbsolutePath());
+ }
+ public void setOutputRealmPath(Path outputRealmPath) {
+ this.outputRealmPath = outputRealmPath.normalize().toAbsolutePath();
+ }
+ public void setFileSystemRealmName(String fileSystemRealmName) {
+ this.fileSystemRealmName = fileSystemRealmName;
+ }
+ public void setKeyStorePath(String keyStorePath) {
+ setKeyStorePath(Path.of(keyStorePath).normalize().toAbsolutePath());
+ }
+ public void setKeyStorePath(Path keyStorePath) {
+ this.keyStorePath = keyStorePath.normalize().toAbsolutePath();
+ }
+ public void setKeyStoreType(String keyStoreType) {
+ this.keyStoreType = keyStoreType;
+ }
+ public void setPassword(String password) {
+ if (password != null) {
+ setPassword(password.toCharArray());
+ } else {
+ setPassword((char[]) null);
+ }
+ }
+ public void setPassword(char[] password) {
+ this.password = password;
+ }
+
+ public void setPasswordEnv(String passwordEnv) {
+ this.passwordEnv = passwordEnv;
+ }
+ public void setKeyPairAlias(String keyPairAlias) {
+ this.keyPairAlias = keyPairAlias;
+ }
+ public void setCredentialStorePath(String credentialStorePath) {
+ setCredentialStorePath(Path.of(credentialStorePath).normalize().toAbsolutePath());
+ }
+ public void setCredentialStorePath(Path credentialStorePath) {
+ this.credentialStorePath = credentialStorePath.normalize().toAbsolutePath();
+ }
+ public void setSecretKeyAlias(String secretKeyAlias) {
+ this.secretKeyAlias = secretKeyAlias;
+ }
+ public void setLevels(String levels) throws NumberFormatException {
+ setLevels(Integer.parseInt(levels));
+ }
+ public void setLevels(Integer levels) {
+ this.levels = levels;
+ }
+ public void setHashEncoding(String hashEncoding) throws IllegalArgumentException {
+ setHashEncoding(Encoding.valueOf(hashEncoding.toUpperCase()));
+ }
+ public void setHashEncoding(Encoding hashEncoding) {
+ this.hashEncoding = hashEncoding;
+ }
+ public void setHashCharset(String hashCharset) {
+ setHashCharset(Charset.forName(hashCharset.toUpperCase()));
+ }
+ public void setHashCharset(Charset hashCharset) {
+ this.hashCharset = hashCharset;
+ }
+ public void setEncoded(String encoded) throws IllegalArgumentException {
+ if (BOOLEAN_ARG_REGEX.matcher(encoded).find()) {
+ setEncoded(Boolean.parseBoolean(encoded));
+ } else throw ElytronToolMessages.msg.encodedMustBeBoolean();
+ }
+ public void setEncoded(Boolean encoded) {
+ this.encoded = encoded;
+ }
+ public void setUpgradeInPlace(Boolean upgradeInPlace) {
+ this.upgradeInPlace = upgradeInPlace;
+ }
+ public void setMissingRequiredValue() {
+ this.missingRequiredValue = true;
+ }
+
+ /** Set when filesystem realm is successfully upgraded. */
+ public void setRealmUpgraded() {
+ this.realmUpgraded = true;
+ }
+
+ void reset(boolean resetMissingValues) {
+ // Required values are set to null if contents are null, or equal "MISSING"
+ if (!Objects.equals(getString(INPUT_LOCATION_PARAM), MISSING)) { inputRealmPath = null; }
+ if (!Objects.equals(getString(KEYSTORE_PARAM), MISSING)) { keyStorePath = null; }
+ if (!Objects.equals(getString(PASSWORD_PARAM), MISSING)) { password = null; }
+
+ outputRealmPath = null;
+ fileSystemRealmName = null;
+ keyStoreType = null;
+ passwordEnv = null;
+ keyPairAlias = null;
+ credentialStorePath = null;
+ secretKeyAlias = null;
+ levels = null;
+ hashEncoding = null;
+ hashCharset = null;
+ encoded = null;
+
+ upgradeInPlace = false;
+ realmUpgraded = false;
+ if (resetMissingValues) {
+ missingRequiredValue = false;
+ }
+ }
+ }
+
+ @Override
+ public void execute(String[] args) throws Exception {
+ setStatus(GENERAL_CONFIGURATION_ERROR);
+ CommandLine cmdLine = parser.parse(options, args, false);
+ setEnableDebug(cmdLine.hasOption(DEBUG_PARAM));
+ if (cmdLine.hasOption(HELP_PARAM)) {
+ help();
+ setStatus(ElytronToolExitStatus_OK);
+ return;
+ }
+ if (cmdLine.hasOption(SILENT_PARAM)) {
+ silentMode = true;
+ }
+ if (cmdLine.hasOption(SUMMARY_PARAM)) {
+ summaryMode = true;
+ summaryString.append(SUMMARY_DIVIDER);
+ summaryString.append(LINE_SEPARATOR);
+ summaryString.append("Summary for execution of Elytron Tool command filesystem-realm-integrity");
+ summaryString.append(LINE_SEPARATOR);
+ summaryString.append(SUMMARY_DIVIDER);
+ summaryString.append(LINE_SEPARATOR);
+ }
+ printDuplicatesWarning(cmdLine);
+
+ String inputRealmPathOption = cmdLine.getOptionValue("i");
+ String outputRealmPathOption = cmdLine.getOptionValue("o");
+ String realmNameOption = cmdLine.getOptionValue("r");
+ String keyStorePathOption = cmdLine.getOptionValue("k");
+ String keyStoreTypeOption = cmdLine.getOptionValue("t");
+ String passwordOption = cmdLine.getOptionValue("p");
+ String passwordEnvOption = cmdLine.getOptionValue("pe");
+ String keyPairAliasOption = cmdLine.getOptionValue("a");
+ String credentialStorePathOption = cmdLine.getOptionValue("c");
+ String secretKeyAliasOption = cmdLine.getOptionValue("s");
+ String levelsOption = cmdLine.getOptionValue("l");
+ String hashEncodingOption = cmdLine.getOptionValue("e");
+ String hashCharsetOption = cmdLine.getOptionValue("u");
+ String encodedOption = cmdLine.getOptionValue("f");
+ String bulkConvertOption = cmdLine.getOptionValue("b");
+
+ if (bulkConvertOption == null) {
+ if (summaryMode) {
+ summaryString.append("Options were specified via CLI, converting single users-roles combination");
+ summaryString.append(LINE_SEPARATOR);
+ }
+ Descriptor descriptor = new Descriptor();
+
+ if (inputRealmPathOption == null) {
+ errorHandler(ElytronToolMessages.msg.inputLocationNotSpecified());
+ } else {
+ Path inputPath = Path.of(inputRealmPathOption).normalize().toAbsolutePath();
+ if (Files.notExists(inputPath)) {
+ errorHandler(ElytronToolMessages.msg.inputLocationDoesNotExist());
+ }
+ descriptor.setInputRealmPath(inputPath);
+ }
+
+ if (outputRealmPathOption != null) {
+ Path outputPath = Path.of(outputRealmPathOption).normalize().toAbsolutePath();
+ Files.createDirectories(outputPath); // Throws nothing if already exists
+ descriptor.setOutputRealmPath(outputPath);
+ }
+
+ descriptor.setFileSystemRealmName(Objects.requireNonNullElse(realmNameOption, DEFAULT_FILESYSTEM_REALM_NAME));
+
+ if (keyStorePathOption == null) {
+ throw ElytronToolMessages.msg.keyStorePathNotSpecified();
+ } else {
+ Path keyStorePath = Path.of(keyStorePathOption);
+ if (Files.notExists(keyStorePath)) {
+ throw ElytronToolMessages.msg.keyStoreDoesNotExist();
+ }
+ descriptor.setKeyStorePath(keyStorePath);
+ }
+
+ descriptor.setKeyStoreType(keyStoreTypeOption);
+
+ if (passwordOption == null && passwordEnvOption == null) {
+ passwordOption = prompt(false, ElytronToolMessages.msg.keyStorePasswordPrompt(), false, null);
+ if (passwordOption == null) {
+ setStatus(GENERAL_CONFIGURATION_ERROR);
+ throw ElytronToolMessages.msg.optionNotSpecified(PASSWORD_PARAM + " or " + PASSWORD_ENV_PARAM);
+ }
+ } else if (passwordEnvOption != null) { // Resolve environment variable
+ descriptor.setPasswordEnv(passwordEnvOption);
+ passwordOption = System.getenv(passwordEnvOption);
+ }
+ descriptor.setPassword(passwordOption);
+
+ descriptor.setKeyPairAlias(keyPairAliasOption);
+
+ if (credentialStorePathOption != null) {
+ Path credentialStorePath = Path.of(credentialStorePathOption);
+ descriptor.setCredentialStorePath(credentialStorePath);
+ }
+
+ descriptor.setSecretKeyAlias(secretKeyAliasOption);
+
+ if (levelsOption != null) {
+ try {
+ descriptor.setLevels(levelsOption);
+ } catch (NumberFormatException e) {
+ errorHandler(e);
+ }
+ }
+
+ if (hashEncodingOption == null) {
+ descriptor.setHashEncoding(Encoding.BASE64);
+ } else {
+ try {
+ descriptor.setHashEncoding(hashEncodingOption);
+ } catch (IllegalArgumentException e) {
+ errorHandler(e);
+ }
+ }
+
+ if (hashCharsetOption == null) {
+ descriptor.setHashCharset(StandardCharsets.UTF_8);
+ } else {
+ try {
+ descriptor.setHashCharset(hashCharsetOption);
+ } catch (IllegalArgumentException e) {
+ errorHandler(e);
+ }
+ }
+
+ if (encodedOption == null) {
+ descriptor.setEncoded(true);
+ } else if (!BOOLEAN_ARG_REGEX.matcher(encodedOption).find()) {
+ throw ElytronToolMessages.msg.encodedMustBeBoolean();
+ } else {
+ descriptor.setEncoded(Boolean.parseBoolean(encodedOption));
+ }
+
+ descriptors.add(descriptor);
+ findMissingRequiredValuesAndSetValues(0, descriptor);
+ } else if (nonBulkConvertOptionSet(inputRealmPathOption, outputRealmPathOption, realmNameOption, keyStorePathOption,
+ keyStoreTypeOption, passwordOption, passwordEnvOption, keyPairAliasOption, credentialStorePathOption,
+ secretKeyAliasOption, levelsOption, hashEncodingOption, hashCharsetOption, encodedOption)) {
+ throw ElytronToolMessages.msg.mutuallyExclusiveOptionsIntegritySpecified();
+ } else {
+ if (summaryMode) {
+ summaryString.append(String.format("Options were specified via descriptor file: %s, converting multiple old filesystem realm", bulkConvertOption));
+ summaryString.append(LINE_SEPARATOR);
+ }
+ parseDescriptorFile(bulkConvertOption);
+ }
+
+ upgradeFileSystemRealm();
+ createWildFlyScript();
+
+ if (summaryMode) {
+ summaryString.append(SUMMARY_DIVIDER);
+ summaryString.append(LINE_SEPARATOR);
+ summaryString.append("End of summary");
+ summaryString.append(LINE_SEPARATOR);
+ summaryString.append(SUMMARY_DIVIDER);
+ System.out.println(summaryString);
+ }
+
+ if (warningOccurred) {
+ setStatus(GENERAL_CONFIGURATION_WARNING);
+ } else {
+ setStatus(ElytronTool.ElytronToolExitStatus_OK);
+ }
+ }
+
+ /** Displays the help screen for the command */
+ @Override
+ public void help() {
+ HelpFormatter help = new HelpFormatter();
+ help.setWidth(WIDTH);
+ help.printHelp(ElytronToolMessages.msg.cmdHelp(getToolCommand(), FILE_SYSTEM_REALM_INTEGRITY_COMMAND),
+ ElytronToolMessages.msg.cmdFileSystemIntegrityHelpHeader(),
+ options,
+ "",
+ true);
+ }
+
+ /**
+ * Prints out a warning message if silentMode is not enabled and adds the warning to the summary
+ * if summaryMode is enabled
+ *
+ * @param warning The warning to be shown
+ */
+ @Override
+ protected void warningHandler(String warning) {
+ warningOccurred = true;
+ if (!silentMode) {
+ System.out.print("WARNING: ");
+ System.out.println(warning);
+ }
+ if (summaryMode) {
+ summaryString.append("WARNING: ");
+ summaryString.append(warning);
+ summaryString.append(LINE_SEPARATOR);
+ }
+ }
+
+ /**
+ * Determines if a summary needs to be printed and prints summary after an error is thrown
+ *
+ * @param e The exception thrown during execution
+ * @throws Exception The exception to be handled by Elytron Tool
+ */
+ @Override
+ protected void errorHandler(Exception e) throws Exception {
+ setStatus(GENERAL_CONFIGURATION_ERROR);
+ if (summaryMode) {
+ summaryString.append("Error was thrown during execution:");
+ summaryString.append(LINE_SEPARATOR);
+ summaryString.append(e.getMessage());
+ System.out.println(LINE_SEPARATOR + summaryString);
+ }
+ throw e;
+ }
+
+ /**
+ * Prints out information found in a descriptor file for summary mode
+ *
+ * @param count The amount of descriptor blocks in the file
+ */
+ private void printDescriptorBlocks(int count) {
+ summaryString.append(LINE_SEPARATOR);
+ summaryString.append(LINE_SEPARATOR);
+ summaryString.append("Found following filesystem realm combinations - MISSING indicates missing required parameter:");
+ summaryString.append(LINE_SEPARATOR);
+ for (int i = 0; i < count; i++) {
+ StringBuilder summary = new StringBuilder();
+ summary.append("\tPrinting summary for block ");
+ summary.append(i + 1);
+ summary.append(LINE_SEPARATOR);
+ Descriptor descriptor = descriptors.get(i);
+ for (String param : PARAMS_LIST) {
+ String paramValue = descriptor.getString(param);
+
+ summary.append("\t\t");
+ summary.append(param);
+ summary.append(" - ");
+
+ if (param.equals(PASSWORD_PARAM)) {
+ summary.append(printPasswordSummary(paramValue));
+ } else {
+ summary.append(descriptor.getString(param));
+ }
+
+ summary.append(LINE_SEPARATOR);
+ }
+
+ summaryString.append(summary);
+ }
+ summaryString.append(LINE_SEPARATOR);
+ }
+
+ private String printPasswordSummary(String paramValue) {
+ if (paramValue == null) {
+ return null;
+ } else if (paramValue.equals(MISSING)) {
+ return MISSING;
+ } else {
+ return "";
+ }
+ }
+
+ /** @return if any provided options are set. Validates that {@code --bulk-convert} is exclusively set. */
+ private boolean nonBulkConvertOptionSet(String... optionValues) {
+ return Arrays.stream(optionValues).anyMatch(Objects::nonNull);
+ }
+
+ /**
+ * Parses options provided in a descriptor file
+ *
+ * @throws Exception Exception to be handled by Elytron Tool
+ */
+ private void parseDescriptorFile(String file) throws Exception {
+ Path path = Path.of(file);
+ if (!Files.isRegularFile(path)) {
+ errorHandler(ElytronToolMessages.msg.fileNotFound(file));
+ }
+
+ Descriptor descriptor = new Descriptor();
+ AtomicInteger count = new AtomicInteger(1);
+ try (Stream stream = Files.lines(path)) {
+ stream.forEach(line -> {
+ if (line.isEmpty()) { // End of descriptor block
+ if (descriptor.getPasswordEnv() != null) {
+ // Password set by environment variable
+ descriptor.setPassword(System.getenv(descriptor.getPasswordEnv()));
+ }
+ copyAddResetDescriptor(count.intValue(), descriptor);
+ count.getAndIncrement();
+
+ } else {
+ String[] parts = line.split(":");
+ String option = parts[0];
+ String arg = parts[1];
+ switch (option) {
+ case INPUT_LOCATION_PARAM:
+ descriptor.setInputRealmPath(arg);
+ break;
+ case OUTPUT_LOCATION_PARAM:
+ descriptor.setOutputRealmPath(arg);
+ break;
+ case REALM_NAME_PARAM:
+ descriptor.setFileSystemRealmName(arg);
+ break;
+ case KEYSTORE_PARAM:
+ descriptor.setKeyStorePath(arg);
+ break;
+ case KEYSTORE_TYPE_PARAM:
+ descriptor.setKeyStoreType(arg);
+ break;
+ case PASSWORD_PARAM:
+ descriptor.setPassword(arg);
+ break;
+ case PASSWORD_ENV_PARAM:
+ descriptor.setPasswordEnv(arg);
+ break;
+ case KEY_PAIR_ALIAS_PARAM:
+ descriptor.setKeyPairAlias(arg);
+ break;
+ case CREDENTIAL_STORE_LOCATION_PARAM:
+ descriptor.setCredentialStorePath(arg);
+ break;
+ case SECRET_KEY_ALIAS_PARAM:
+ descriptor.setSecretKeyAlias(arg);
+ break;
+ case LEVELS_PARAM:
+ descriptor.setLevels(arg);
+ break;
+ case HASH_ENCODING_PARAM:
+ descriptor.setHashEncoding(arg);
+ break;
+ case HASH_CHARSET_PARAM:
+ descriptor.setHashCharset(arg);
+ break;
+ }
+ }
+ });
+ } catch (Exception e) {
+ errorHandler(e);
+ }
+
+ copyAddResetDescriptor(count.intValue(), descriptor);
+ if (summaryMode) {
+ printDescriptorBlocks(count.intValue());
+ }
+ count.getAndIncrement();
+ }
+
+ /**
+ * Validates a {@link Descriptor} and clones it into the list of descriptors
+ *
+ * @param original The original descriptor that is continually modified
+ */
+ private void copyAddResetDescriptor(int count, Descriptor original) {
+ findMissingRequiredValuesAndSetValues(count, original);
+
+ descriptors.add(new Descriptor(original));
+ original.reset(true);
+ }
+
+ /**
+ * Determines if the current descriptor block is missing any required values
+ * and sets defaults for optional values. If a required value is missing,
+ * prints a warning message and resets the descriptor block.
+ *
+ * @param count The index of the current descriptor block in {@code descriptors}.
+ */
+ private void findMissingRequiredValuesAndSetValues(int count, Descriptor descriptor) {
+ if (descriptor.getInputRealmPath() == null) {
+ warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockInputLocation(count));
+ descriptor.setInputRealmPath(MISSING);
+ descriptor.setMissingRequiredValue();
+ }
+ if (descriptor.getKeyStorePath() == null) {
+ warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockKeyStorePath(count));
+ descriptor.setKeyStorePath(MISSING);
+ descriptor.setMissingRequiredValue();
+ }
+ if (descriptor.getPassword() == null) {
+ // Password is loaded from environment variable after parsing CLI options or bulk conversion block
+ warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockPassword(count));
+ descriptor.setPassword(MISSING);
+ descriptor.setMissingRequiredValue();
+ }
+ if (descriptor.getFileSystemRealmName() == null) { // Don't use zero-index for realm name
+ if (count == 0) {
+ descriptor.setFileSystemRealmName(DEFAULT_FILESYSTEM_REALM_NAME);
+ } else {
+ descriptor.setFileSystemRealmName(DEFAULT_FILESYSTEM_REALM_NAME + "-" + UUID.randomUUID());
+ }
+ }
+ if (descriptor.getKeyPairAlias() == null) descriptor.setKeyPairAlias(DEFAULT_KEY_PAIR_ALIAS);
+ if (descriptor.getLevels() == null) descriptor.setLevels(DEFAULT_LEVELS);
+ if (descriptor.getHashEncoding() == null) descriptor.setHashEncoding(Encoding.BASE64);
+ if (descriptor.getHashCharset() == null) descriptor.setHashCharset(StandardCharsets.UTF_8);
+ if (descriptor.getEncoded() == null) descriptor.setEncoded(true);
+
+ if (descriptor.getOutputRealmPath() == null) {
+ descriptor.setUpgradeInPlace(true);
+ }
+ if (descriptor.getMissingRequiredValue()) {
+ descriptor.reset(false);
+ }
+ }
+
+ /**
+ * Handles upgrading the Elytron filesystem realm from the descriptor array
+ *
+ * @throws Exception Exception to be handled by Elytron Tool
+ */
+ private void upgradeFileSystemRealm() throws Exception {
+ int count = 0;
+ for (Descriptor descriptor : descriptors) {
+ count++;
+ if (descriptor.getMissingRequiredValue()) {
+ warningHandler(ElytronToolMessages.msg.skippingDescriptorBlock(count, "missing required parameter"));
+ continue;
+ }
+
+ System.out.println(ElytronToolMessages.msg.fileSystemRealmIntegrityCreatingRealm(descriptor.getString(INPUT_LOCATION_PARAM)));
+
+ // Load key pair
+ KeyPair keyPair = getKeyPair(descriptor.getKeyStorePath(), descriptor.getKeyStoreType(), descriptor.getKeyPairAlias(),
+ descriptor.getPassword(), count);
+ if (keyPair == null) continue;
+
+ // Configure existing and new filesystem realms
+ Path inputPath = descriptor.getInputRealmPath();
+ Path outputPath = descriptor.getOutputRealmPath();
+ if (inputPath == null) {
+ warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockInputLocation(count));
+ continue;
+ }
+
+ // Configure output path for realm name or in-place upgrade
+ if (descriptor.getUpgradeInPlace()) {
+ Path backupPath = backupInputFileSystemRealm(descriptor, count);
+ if (backupPath == null) {
+ outputPath = Path.of(inputPath.toString().replaceFirst(Pattern.quote(FILE_SEPARATOR + "*$"), "") + "-with-integrity");
+
+ descriptor.setUpgradeInPlace(false);
+ warningHandler(ElytronToolMessages.msg.unableToUpgradeInPlace(inputPath.toString(), outputPath.toString()));
+ } else {
+ outputPath = inputPath;
+ inputPath = backupPath;
+ }
+
+ // Update output path for CLI script generation
+ descriptor.setOutputRealmPath(outputPath);
+ } else {
+ outputPath = outputPath.resolve(descriptor.getFileSystemRealmName());
+ }
+
+ FileSystemSecurityRealmBuilder inputFileSystemRealmBuilder = FileSystemSecurityRealm.builder()
+ .setRoot(inputPath)
+ .setLevels(descriptor.getLevels())
+ .setHashEncoding(descriptor.getHashEncoding())
+ .setHashCharset(descriptor.getHashCharset())
+ .setEncoded(descriptor.getEncoded())
+ .setProviders(ELYTRON_KS_PASS_PROVIDERS);
+
+ FileSystemSecurityRealmBuilder outputFileSystemRealmBuilder = FileSystemSecurityRealm.builder()
+ .setRoot(outputPath)
+ .setPrivateKey(keyPair.getPrivate())
+ .setPublicKey(keyPair.getPublic())
+ .setLevels(descriptor.getLevels())
+ .setHashCharset(descriptor.getHashCharset())
+ .setProviders(ELYTRON_KS_PASS_PROVIDERS);
+
+ // Load encryption SecretKey if provided
+ if (descriptor.getCredentialStorePath() != null) {
+ SecretKey secretKey = getSecretKey(false, descriptor.getString(CREDENTIAL_STORE_LOCATION_PARAM),
+ descriptor.getSecretKeyAlias(), false, count);
+ if (secretKey != null) {
+ inputFileSystemRealmBuilder.setSecretKey(secretKey);
+ outputFileSystemRealmBuilder.setSecretKey(secretKey);
+ } else continue;
+ }
+
+ FileSystemSecurityRealm inputRealm = inputFileSystemRealmBuilder.build();
+ if (!inputRealm.getRealmIdentityIterator().hasNext()) {
+ warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockEmptyRealm(count));
+ continue;
+ }
+
+ FileSystemRealmUtil.cloneIdentitiesToNewRealm(
+ inputRealm,
+ outputFileSystemRealmBuilder.build());
+
+ descriptor.setRealmUpgraded();
+ }
+ }
+
+ /**
+ * Generates the CLI commands the user must run for Elytron to recognize
+ * and use the new filesystem-realm, and saves them to a file
+ */
+ private void createWildFlyScript() throws Exception {
+ int counter = 0;
+ for (Descriptor descriptor : descriptors) {
+ counter++;
+ if (!descriptor.getRealmUpgraded()) {
+ continue;
+ }
+
+ String fileSystemRealmName = descriptor.getFileSystemRealmName();
+ Path outputRealmPath = descriptor.getOutputRealmPath();
+ boolean upgradeInPlace = descriptor.getUpgradeInPlace();
+
+ String createScriptCheck = "";
+ Path scriptPath = Path.of(String.format("%s/%s.cli", outputRealmPath, fileSystemRealmName));
+
+ // Ask to overwrite CLI script, if already exists
+ if(scriptPath.toFile().exists()) {
+ createScriptCheck = prompt(
+ true,
+ ElytronToolMessages.msg.shouldFileBeOverwritten(scriptPath.toString()),
+ false,
+ null
+ );
+ if (createScriptCheck.trim().isEmpty()) createScriptCheck = "n";
+ }
+
+ boolean overwriteScript = createScriptCheck.isEmpty() || createScriptCheck.toLowerCase().startsWith("y");
+ if (!overwriteScript) {
+ do {
+ scriptPath = Path.of(String.format("%s/%s.cli",
+ outputRealmPath,
+ fileSystemRealmName + "-" + UUID.randomUUID()));
+ } while (scriptPath.toFile().exists());
+ }
+
+ if (summaryMode) {
+ summaryString.append(String.format("Configured script for WildFly named %s.cli at %s.", fileSystemRealmName, outputRealmPath));
+ summaryString.append(LINE_SEPARATOR);
+ summaryString.append(String.format("Name of filesystem-realm: %s", fileSystemRealmName));
+ summaryString.append(LINE_SEPARATOR);
+ }
+
+ ArrayList scriptLines = new ArrayList<>(Arrays.asList(
+ String.format("/subsystem=elytron/key-store=%s:add(path=%s, credential-reference={clear-text=\"%s\"}%s)",
+ "mykeystore"+counter,
+ descriptor.getKeyStorePath(),
+ descriptor.getString(PASSWORD_PARAM),
+ descriptor.getKeyStoreType() != null ? ", type="+descriptor.getKeyStoreType() : ""),
+ String.format("/subsystem=elytron/filesystem-realm=%s:add(path=%s%s%s, key-store=%s, key-store-alias=%s%s%s)",
+ fileSystemRealmName,
+ upgradeInPlace ? outputRealmPath : outputRealmPath.toString() + FILE_SEPARATOR + fileSystemRealmName,
+ descriptor.getCredentialStorePath() != null ? ", credential-store=mycredstore" + counter : "",
+ descriptor.getSecretKeyAlias() != null ? ", secret-key="+descriptor.getSecretKeyAlias() : "",
+ "mykeystore"+counter,
+ descriptor.getKeyPairAlias(),
+ !descriptor.getLevels().equals(DEFAULT_LEVELS) ? ", levels="+descriptor.getLevels() : "",
+ descriptor.getHashCharset() != StandardCharsets.UTF_8 ? ", hash-charset="+descriptor.getHashCharset() : "")
+ ));
+
+ if (descriptor.getCredentialStorePath() != null) {
+ // Credential store must be added before encrypted realm
+ scriptLines.add(1, String.format("/subsystem=elytron/secret-key-credential-store=%s:add(path=%s)",
+ "mycredstore"+counter, descriptor.getCredentialStorePath()));
+ }
+
+ if (overwriteScript) { // Create a new script file, or append the existing one
+ Files.write(scriptPath, scriptLines, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ } else {
+ Files.write(scriptPath, scriptLines, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
+ }
+ }
+ }
+
+ /**
+ * Backup input filesystem realm to a new location, and delete original folder
+ *
+ * @param descriptor the current descriptor block
+ * @return the {@link Path} of the backup directory, in format {@code -backup[number]},
+ * or {@code null} if the directory could not be backed up.
+ * @throws Exception if an error occurs while deleting the old directory.
+ */
+ private Path backupInputFileSystemRealm(Descriptor descriptor, int count) throws Exception {
+ Path originalDirectory = descriptor.getInputRealmPath();
+ Path backupDirectory = Path.of(descriptor.getString(INPUT_LOCATION_PARAM)
+ .replaceFirst(Pattern.quote(FILE_SEPARATOR + "*$"), "") + "-backup");
+
+ // Append number if directory already exists
+ if (backupDirectory.toFile().exists()) {
+ Path numBackupDirectory;
+ do {
+ numBackupDirectory = Path.of(backupDirectory + "-" + UUID.randomUUID());
+ } while (numBackupDirectory.toFile().exists());
+
+ backupDirectory = numBackupDirectory;
+ }
+
+ // Copy the filesystem realm
+ try {
+ final Path finalBackupDirectory = backupDirectory;
+ Files.walkFileTree(originalDirectory, new SimpleFileVisitor<>() {
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+ Files.createDirectories(finalBackupDirectory.resolve(originalDirectory.relativize(dir)));
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ Files.copy(file, finalBackupDirectory.resolve(originalDirectory.relativize(file)));
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ } catch (IOException | RuntimeException e) {
+ return null;
+ }
+
+ // Delete current contents if backup is successful
+ try {
+ Files.walkFileTree(originalDirectory, new SimpleFileVisitor<>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ if (file.toFile().delete()) {
+ return FileVisitResult.CONTINUE;
+ } else {
+ throw new IOException("Unable to delete " + file);
+ }
+ }
+ });
+ } catch (IOException | RuntimeException e) {
+ errorHandler(e);
+ }
+
+ System.out.println(ElytronToolMessages.msg.fileSystemRealmIntegrityInPlaceBackup(count, backupDirectory.toString()));
+ return backupDirectory;
+ }
+}
\ No newline at end of file
diff --git a/tool/src/test/java/org/wildfly/security/tool/FileSystemEncryptRealmCommandTest.java b/tool/src/test/java/org/wildfly/security/tool/FileSystemEncryptRealmCommandTest.java
index 2674a77da40..c273cdffdfa 100644
--- a/tool/src/test/java/org/wildfly/security/tool/FileSystemEncryptRealmCommandTest.java
+++ b/tool/src/test/java/org/wildfly/security/tool/FileSystemEncryptRealmCommandTest.java
@@ -19,7 +19,7 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
-import static org.wildfly.security.tool.FileSystemEncryptRealmCommand.ELYTRON_PASSWORD_PROVIDERS;
+import static org.wildfly.security.tool.Command.ELYTRON_KS_PASS_PROVIDERS;
import java.io.File;
import java.io.FileNotFoundException;
@@ -197,7 +197,7 @@ public void testSingleUserAndVerify() throws Exception {
FileSystemSecurityRealm securityRealm = FileSystemSecurityRealm.builder()
.setRoot(Paths.get(outputLocation, fileSystemRealmName))
.setLevels(2)
- .setProviders(ELYTRON_PASSWORD_PROVIDERS)
+ .setProviders(ELYTRON_KS_PASS_PROVIDERS)
.setSecretKey(key)
.build();
ModifiableRealmIdentity existingIdentity = securityRealm.getRealmIdentityForUpdate(new NamePrincipal("hello"));