Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion multiapps-controller-core/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
exports org.cloudfoundry.multiapps.controller.core.validators.parameters;
exports org.cloudfoundry.multiapps.controller.core.validators.parameters.v2;
exports org.cloudfoundry.multiapps.controller.core.validators.parameters.v3;
exports org.cloudfoundry.multiapps.controller.core.security.encryption;

requires transitive jakarta.persistence;
requires transitive org.cloudfoundry.multiapps.controller.client;
Expand All @@ -61,6 +62,7 @@
requires org.apache.httpcomponents.client5.httpclient5;
requires org.apache.httpcomponents.core5.httpcore5;
requires org.apache.tika.core;
requires org.bouncycastle.fips.core;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not able to run the test in IDE? Could you check. I had to exclude:

com.aliyun.oss
aliyun-sdk-oss



org.ini4j
ini4j


javax.xml.bind
jaxb-api



org.bouncycastle
bcprov-jdk18on


${aliyun-sdk-oss.version}

requires org.cloudfoundry.multiapps.common;
requires org.cloudfoundry.multiapps.controller.api;
requires org.slf4j;
Expand All @@ -79,6 +81,5 @@

requires static java.compiler;
requires static org.immutables.value;
requires spring.security.oauth2.client;

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,19 @@ public class Constants {

public static final String B3_TRACE_ID_HEADER = "X-B3-TraceId";
public static final String B3_SPAN_ID_HEADER = "X-B3-SpanId";
public static final String CIPHER_TRANSFORMATION_NAME = "AES/GCM/NoPadding";
public static final String ENCRYPTION_DECRYPTION_ALGORITHM_NAME = "AES";

public static final int TOKEN_SERVICE_DELETION_CORE_POOL_SIZE = 1;
public static final int TOKEN_SERVICE_DELETION_MAXIMUM_POOL_SIZE = 3;
public static final int TOKEN_SERVICE_DELETION_KEEP_ALIVE_THREAD_IN_SECONDS = 30;
//The Initialisation Vector (also called nonce) is 12-bytes (96 bits), because that is the standard and recommended length for AES-GCM primarily for performance and simplicity of the implementation -
//this is the exact length that balances a sufficiently large uniqueness space with maximum computational efficiency according to the NIST specifications for the GCM variant of AES
public static final int INITIALISATION_VECTOR_LENGTH = 12;
public static final int INITIALIZATION_VECTOR_POSITION = 12;
//The authentication tag in AES-GCM is always 128 bits because every version of the AES algorithm (including AES-256) processes data in fixed 128-bit blocks,
//which dictates the size of the final authentication result required by the GCM mode's internals
public static final int GCM_AUTHENTICATION_TAG_LENGTH = 128;

public static final String APP_FEATURE_SSH = "ssh";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public final class Messages {
public static final String BUILDPACKS_NOT_ALLOWED_WITH_DOCKER = "Buildpacks must not be provided when lifecycle is set to 'docker'.";
public static final String EXTENSION_DESCRIPTORS_COULD_NOT_BE_PARSED_TO_VALID_YAML = "Extension descriptor(s) could not be parsed as a valid YAML file. These descriptors may fail future deployments once stricter validation is enforced. Please review and correct them now to avoid future issues. Use at your own risk";
public static final String UNSUPPORTED_FILE_FORMAT = "Unsupported file format! \"{0}\" detected";
public static final String ENCRYPTION_HAS_FAILED = "Encryption has failed! Errored with \"{0}\"";
public static final String DECRYPTION_HAS_FAILED = "Decryption has failed! Errored with \"{0}\"";

// Warning messages
public static final String ENVIRONMENT_VARIABLE_IS_NOT_SET_USING_DEFAULT = "Environment variable \"{0}\" is not set. Using default \"{1}\"...";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import org.cloudfoundry.multiapps.controller.core.Messages;
import org.cloudfoundry.multiapps.controller.core.cf.CloudHandlerFactory;
import org.cloudfoundry.multiapps.controller.core.security.serialization.SecureSerialization;
import org.cloudfoundry.multiapps.controller.core.security.serialization.DynamicSecureSerialization;
import org.cloudfoundry.multiapps.controller.core.security.serialization.SecureSerializationFactory;
import org.cloudfoundry.multiapps.controller.core.util.UserMessageLogger;
import org.cloudfoundry.multiapps.mta.handlers.v2.DescriptorMerger;
import org.cloudfoundry.multiapps.mta.handlers.v2.DescriptorValidator;
Expand All @@ -28,11 +29,13 @@ public MtaDescriptorMerger(CloudHandlerFactory handlerFactory, Platform platform
this.userMessageLogger = userMessageLogger;
}

public DeploymentDescriptor merge(DeploymentDescriptor deploymentDescriptor, List<ExtensionDescriptor> extensionDescriptors) {
public DeploymentDescriptor merge(DeploymentDescriptor deploymentDescriptor, List<ExtensionDescriptor> extensionDescriptors,
List<String> parameterNamesToBeMasked) {
DescriptorValidator validator = handlerFactory.getDescriptorValidator();
validator.validateDeploymentDescriptor(deploymentDescriptor, platform);
validator.validateExtensionDescriptors(extensionDescriptors, deploymentDescriptor);

DynamicSecureSerialization dynamicSecureSerialization = SecureSerializationFactory.ofAdditionalValues(parameterNamesToBeMasked);
DescriptorMerger merger = handlerFactory.getDescriptorMerger();

// Merge the passed set of descriptors into one deployment descriptor. The deployment descriptor at the root of
Expand All @@ -45,7 +48,7 @@ public DeploymentDescriptor merge(DeploymentDescriptor deploymentDescriptor, Lis

deploymentDescriptor = handlerFactory.getDescriptorParametersCompatibilityValidator(mergedDescriptor, userMessageLogger)
.validate();
logDebug(Messages.MERGED_DESCRIPTOR, SecureSerialization.toJson(deploymentDescriptor));
logDebug(Messages.MERGED_DESCRIPTOR, dynamicSecureSerialization.toJson(deploymentDescriptor));

return deploymentDescriptor;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.cloudfoundry.multiapps.controller.core.security.encryption;

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.text.MessageFormat;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
import org.cloudfoundry.multiapps.common.SLException;
import org.cloudfoundry.multiapps.controller.core.Constants;
import org.cloudfoundry.multiapps.controller.core.Messages;

public class AesEncryptionUtil {

public static byte[] encrypt(String plainText, byte[] encryptionKey) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor spelling issue: the variable name uses “Cypher” instead of “Cipher”. Consider updating the variable names to use “Cipher” for clarity and consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for pointing out!

try {
if (plainText == null) {
plainText = "";
}
byte[] gcmInitialisationVector = new byte[Constants.INITIALISATION_VECTOR_LENGTH];
new SecureRandom().nextBytes(gcmInitialisationVector);

Cipher cipherObject = setUpCipherObject(encryptionKey, gcmInitialisationVector, true);

byte[] cipherValue = cipherObject.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

byte[] combinedCipherValueAndInitialisationVector = new byte[gcmInitialisationVector.length + cipherValue.length];

System.arraycopy(gcmInitialisationVector, 0, combinedCipherValueAndInitialisationVector, 0, gcmInitialisationVector.length);
System.arraycopy(cipherValue, 0, combinedCipherValueAndInitialisationVector, gcmInitialisationVector.length,
cipherValue.length);

return combinedCipherValueAndInitialisationVector;
} catch (Exception e) {
throw new SLException(MessageFormat.format(Messages.ENCRYPTION_HAS_FAILED, e.getMessage()), e);
}
}

public static String decrypt(byte[] encryptedValue, byte[] encryptionKey) {
try {
byte[] gcmInitialisationVector = new byte[Constants.INITIALISATION_VECTOR_LENGTH];
System.arraycopy(encryptedValue, 0, gcmInitialisationVector, 0,
gcmInitialisationVector.length);

byte[] cipherValue = new byte[encryptedValue.length - Constants.INITIALISATION_VECTOR_LENGTH];
System.arraycopy(encryptedValue, Constants.INITIALIZATION_VECTOR_POSITION, cipherValue, 0, cipherValue.length);

Cipher cipherObject = setUpCipherObject(encryptionKey, gcmInitialisationVector, false);

byte[] resultInBytes = cipherObject.doFinal(cipherValue);
return new String(resultInBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new SLException(MessageFormat.format(Messages.DECRYPTION_HAS_FAILED, e.getMessage()), e);
}
}

private static Cipher setUpCipherObject(byte[] encryptionKey, byte[] gcmInitialisationVector, boolean shouldEncrypt)
throws InvalidAlgorithmParameterException, InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException,
NoSuchProviderException {
Cipher cipherObject = Cipher.getInstance(Constants.CIPHER_TRANSFORMATION_NAME, BouncyCastleFipsProvider.PROVIDER_NAME);
SecretKeySpec secretKeySpec = new SecretKeySpec(encryptionKey, Constants.ENCRYPTION_DECRYPTION_ALGORITHM_NAME);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(Constants.GCM_AUTHENTICATION_TAG_LENGTH, gcmInitialisationVector);

if (shouldEncrypt) {
cipherObject.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec);
} else {
cipherObject.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
}

return cipherObject;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.cloudfoundry.multiapps.controller.core.security.serialization;

import org.cloudfoundry.multiapps.controller.core.security.serialization.model.DeploymentDescriptorSerializer;
import org.cloudfoundry.multiapps.controller.core.security.serialization.model.ModuleSerializer;
import org.cloudfoundry.multiapps.controller.core.security.serialization.model.ProvidedDependencySerializer;
import org.cloudfoundry.multiapps.controller.core.security.serialization.model.RequiredDependencySerializer;
import org.cloudfoundry.multiapps.controller.core.security.serialization.model.ResourceSerializer;
import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor;
import org.cloudfoundry.multiapps.mta.model.Module;
import org.cloudfoundry.multiapps.mta.model.ProvidedDependency;
import org.cloudfoundry.multiapps.mta.model.RequiredDependency;
import org.cloudfoundry.multiapps.mta.model.Resource;
import org.cloudfoundry.multiapps.mta.model.VersionedEntity;

public final class DynamicSecureSerialization {

private final SecureSerializerConfiguration secureSerializerConfiguration;

public DynamicSecureSerialization(SecureSerializerConfiguration secureSerializerConfiguration) {
this.secureSerializerConfiguration = secureSerializerConfiguration;
}

public String toJson(Object object) {
SecureJsonSerializer secureJsonSerializer = createDynamicJsonSerializer(object);
return secureJsonSerializer.serialize(object);
}

private SecureJsonSerializer createDynamicJsonSerializer(Object object) {
SecureJsonSerializer secureJsonSerializer = createDynamicJsonSerializerForVersionedEntity(object);
if (secureJsonSerializer == null) {
return new SecureJsonSerializer(secureSerializerConfiguration);
}

return secureJsonSerializer;
}

private SecureJsonSerializer createDynamicJsonSerializerForVersionedEntity(Object object) {
if (object instanceof VersionedEntity) {
return createDynamicJsonSerializerForVersionedEntity((VersionedEntity) object);
}

return null;
}

private SecureJsonSerializer createDynamicJsonSerializerForVersionedEntity(VersionedEntity versionedEntity) {
if (versionedEntity.getMajorSchemaVersion() < 3) {
return null;
}

if (versionedEntity instanceof DeploymentDescriptor) {
return new DeploymentDescriptorSerializer(secureSerializerConfiguration);
}

if (versionedEntity instanceof Module) {
return new ModuleSerializer(secureSerializerConfiguration);
}

if (versionedEntity instanceof ProvidedDependency) {
return new ProvidedDependencySerializer(secureSerializerConfiguration);
}

if (versionedEntity instanceof RequiredDependency) {
return new RequiredDependencySerializer(secureSerializerConfiguration);
}

if (versionedEntity instanceof Resource) {
return new ResourceSerializer(secureSerializerConfiguration);
}

return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.cloudfoundry.multiapps.controller.core.security.serialization;

import java.util.Collection;

public final class SecureSerializationFactory {

private SecureSerializationFactory() {

}

public static DynamicSecureSerialization ofAdditionalValues(Collection<String> additionalSensitiveElementNames) {
SecureSerializerConfiguration secureSerializerConfigurationWithAdditionalValues = new SecureSerializerConfiguration();

secureSerializerConfigurationWithAdditionalValues.setAdditionalSensitiveElementNames(additionalSensitiveElementNames);
return new DynamicSecureSerialization(secureSerializerConfigurationWithAdditionalValues);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;

public class SecureSerializerConfiguration {

Expand All @@ -17,8 +20,25 @@ public class SecureSerializerConfiguration {
private Collection<String> sensitiveElementNames = DEFAULT_SENSITIVE_NAMES;
private Collection<String> sensitiveElementPaths = Collections.emptyList();

private Collection<String> additionalSensitiveElementNames = Collections.emptyList();

public Collection<String> getSensitiveElementNames() {
return sensitiveElementNames;
if (CollectionUtils.isEmpty(additionalSensitiveElementNames)) {
return sensitiveElementNames;
}

Set<String> mergedSensitiveElementNames = new HashSet<>(sensitiveElementNames);

for (String additionalSensitiveElement : additionalSensitiveElementNames) {
boolean isNotExistent = mergedSensitiveElementNames.stream()
.noneMatch(sensitiveElement -> sensitiveElement.equalsIgnoreCase(
additionalSensitiveElement));
Comment on lines +32 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it needed to have this check? Why not adding them as they are unique in the set?

if (isNotExistent) {
mergedSensitiveElementNames.add(additionalSensitiveElement);
}
}

return mergedSensitiveElementNames;
}

public Collection<String> getSensitiveElementPaths() {
Expand All @@ -33,6 +53,10 @@ public void setSensitiveElementNames(Collection<String> sensitiveElementNames) {
this.sensitiveElementNames = sensitiveElementNames;
}

public void setAdditionalSensitiveElementNames(Collection<String> additionalSensitiveElementNames) {
this.additionalSensitiveElementNames = additionalSensitiveElementNames;
}

public void setSensitiveElementPaths(Collection<String> sensitiveElementPaths) {
this.sensitiveElementPaths = sensitiveElementPaths;
}
Expand Down
Loading
Loading