Skip to content

Commit d064584

Browse files
authored
Enabling using secret values during a deployment (#1755)
* Enabling using secret values during a deployment LMCROSSITXSADEPLOY-2301
1 parent f0cad0f commit d064584

File tree

112 files changed

+3029
-213
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+3029
-213
lines changed

multiapps-controller-api/src/main/java/org/cloudfoundry/multiapps/controller/api/model/AsyncUploadResult.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package org.cloudfoundry.multiapps.controller.api.model;
22

3+
import java.util.List;
4+
35
import com.fasterxml.jackson.annotation.JsonProperty;
46
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
57
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
6-
78
import io.swagger.annotations.ApiModelProperty;
89
import org.cloudfoundry.multiapps.common.Nullable;
910
import org.immutables.value.Value;
1011

11-
import java.util.List;
12-
1312
@Value.Immutable
1413
@JsonSerialize(as = ImmutableAsyncUploadResult.class)
1514
@JsonDeserialize(as = ImmutableAsyncUploadResult.class)
@@ -47,6 +46,11 @@ enum ClientAction {
4746
@JsonProperty("mta_id")
4847
String getMtaId();
4948

49+
@Nullable
50+
@ApiModelProperty
51+
@JsonProperty("schema_version")
52+
String getSchemaVersion();
53+
5054
@Nullable
5155
@ApiModelProperty
5256
@JsonProperty("client_actions")

multiapps-controller-core/src/main/java/module-info.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
exports org.cloudfoundry.multiapps.controller.core.validators.parameters;
3939
exports org.cloudfoundry.multiapps.controller.core.validators.parameters.v2;
4040
exports org.cloudfoundry.multiapps.controller.core.validators.parameters.v3;
41+
exports org.cloudfoundry.multiapps.controller.core.security.encryption;
4142

4243
requires transitive jakarta.persistence;
4344
requires transitive org.cloudfoundry.multiapps.controller.client;
@@ -61,6 +62,7 @@
6162
requires org.apache.httpcomponents.client5.httpclient5;
6263
requires org.apache.httpcomponents.core5.httpcore5;
6364
requires org.apache.tika.core;
65+
requires org.bouncycastle.fips.core;
6466
requires org.cloudfoundry.multiapps.common;
6567
requires org.cloudfoundry.multiapps.controller.api;
6668
requires org.slf4j;
@@ -79,6 +81,5 @@
7981

8082
requires static java.compiler;
8183
requires static org.immutables.value;
82-
requires spring.security.oauth2.client;
8384

8485
}

multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Constants.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,19 @@ public class Constants {
2929

3030
public static final String B3_TRACE_ID_HEADER = "X-B3-TraceId";
3131
public static final String B3_SPAN_ID_HEADER = "X-B3-SpanId";
32+
public static final String CIPHER_TRANSFORMATION_NAME = "AES/GCM/NoPadding";
33+
public static final String ENCRYPTION_DECRYPTION_ALGORITHM_NAME = "AES";
3234

3335
public static final int TOKEN_SERVICE_DELETION_CORE_POOL_SIZE = 1;
3436
public static final int TOKEN_SERVICE_DELETION_MAXIMUM_POOL_SIZE = 3;
3537
public static final int TOKEN_SERVICE_DELETION_KEEP_ALIVE_THREAD_IN_SECONDS = 30;
38+
//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 -
39+
//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
40+
public static final int INITIALISATION_VECTOR_LENGTH = 12;
41+
public static final int INITIALISATION_VECTOR_POSITION = 12;
42+
//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,
43+
//which dictates the size of the final authentication result required by the GCM mode's internals
44+
public static final int GCM_AUTHENTICATION_TAG_LENGTH = 128;
3645

3746
public static final String APP_FEATURE_SSH = "ssh";
3847

multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ public final class Messages {
9393
public static final String BUILDPACKS_NOT_ALLOWED_WITH_DOCKER = "Buildpacks must not be provided when lifecycle is set to 'docker'.";
9494
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";
9595
public static final String UNSUPPORTED_FILE_FORMAT = "Unsupported file format! \"{0}\" detected";
96+
public static final String ENCRYPTION_HAS_FAILED = "Encryption has failed! Errored with \"{0}\"";
97+
public static final String DECRYPTION_HAS_FAILED = "Decryption has failed! Errored with \"{0}\"";
9698

9799
// Warning messages
98100
public static final String ENVIRONMENT_VARIABLE_IS_NOT_SET_USING_DEFAULT = "Environment variable \"{0}\" is not set. Using default \"{1}\"...";

multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaDescriptorMerger.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import org.cloudfoundry.multiapps.controller.core.Messages;
66
import org.cloudfoundry.multiapps.controller.core.cf.CloudHandlerFactory;
7-
import org.cloudfoundry.multiapps.controller.core.security.serialization.SecureSerialization;
7+
import org.cloudfoundry.multiapps.controller.core.security.serialization.DynamicSecureSerialization;
8+
import org.cloudfoundry.multiapps.controller.core.security.serialization.SecureSerializationFactory;
89
import org.cloudfoundry.multiapps.controller.core.util.UserMessageLogger;
910
import org.cloudfoundry.multiapps.mta.handlers.v2.DescriptorMerger;
1011
import org.cloudfoundry.multiapps.mta.handlers.v2.DescriptorValidator;
@@ -28,11 +29,13 @@ public MtaDescriptorMerger(CloudHandlerFactory handlerFactory, Platform platform
2829
this.userMessageLogger = userMessageLogger;
2930
}
3031

31-
public DeploymentDescriptor merge(DeploymentDescriptor deploymentDescriptor, List<ExtensionDescriptor> extensionDescriptors) {
32+
public DeploymentDescriptor merge(DeploymentDescriptor deploymentDescriptor, List<ExtensionDescriptor> extensionDescriptors,
33+
List<String> parameterNamesToBeMasked) {
3234
DescriptorValidator validator = handlerFactory.getDescriptorValidator();
3335
validator.validateDeploymentDescriptor(deploymentDescriptor, platform);
3436
validator.validateExtensionDescriptors(extensionDescriptors, deploymentDescriptor);
3537

38+
DynamicSecureSerialization dynamicSecureSerialization = SecureSerializationFactory.ofAdditionalValues(parameterNamesToBeMasked);
3639
DescriptorMerger merger = handlerFactory.getDescriptorMerger();
3740

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

4649
deploymentDescriptor = handlerFactory.getDescriptorParametersCompatibilityValidator(mergedDescriptor, userMessageLogger)
4750
.validate();
48-
logDebug(Messages.MERGED_DESCRIPTOR, SecureSerialization.toJson(deploymentDescriptor));
51+
logDebug(Messages.MERGED_DESCRIPTOR, dynamicSecureSerialization.toJson(deploymentDescriptor));
4952

5053
return deploymentDescriptor;
5154
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package org.cloudfoundry.multiapps.controller.core.security.encryption;
2+
3+
import java.nio.charset.StandardCharsets;
4+
import java.security.InvalidAlgorithmParameterException;
5+
import java.security.InvalidKeyException;
6+
import java.security.NoSuchAlgorithmException;
7+
import java.security.NoSuchProviderException;
8+
import java.security.SecureRandom;
9+
import java.text.MessageFormat;
10+
import javax.crypto.Cipher;
11+
import javax.crypto.NoSuchPaddingException;
12+
import javax.crypto.spec.GCMParameterSpec;
13+
import javax.crypto.spec.SecretKeySpec;
14+
15+
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
16+
import org.cloudfoundry.multiapps.common.SLException;
17+
import org.cloudfoundry.multiapps.controller.core.Constants;
18+
import org.cloudfoundry.multiapps.controller.core.Messages;
19+
20+
public class AesEncryptionUtil {
21+
22+
public static byte[] encrypt(String plainText, byte[] encryptionKey) {
23+
try {
24+
byte[] gcmInitialisationVector = new byte[Constants.INITIALISATION_VECTOR_LENGTH];
25+
new SecureRandom().nextBytes(gcmInitialisationVector);
26+
27+
Cipher cipherObject = setUpCipherObject(encryptionKey, gcmInitialisationVector, Cipher.ENCRYPT_MODE);
28+
29+
byte[] cipherValue = cipherObject.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
30+
31+
byte[] combinedCipherValueAndInitialisationVector = new byte[gcmInitialisationVector.length + cipherValue.length];
32+
33+
System.arraycopy(gcmInitialisationVector, 0, combinedCipherValueAndInitialisationVector, 0, gcmInitialisationVector.length);
34+
System.arraycopy(cipherValue, 0, combinedCipherValueAndInitialisationVector, gcmInitialisationVector.length,
35+
cipherValue.length);
36+
37+
return combinedCipherValueAndInitialisationVector;
38+
} catch (Exception e) {
39+
throw new SLException(MessageFormat.format(Messages.ENCRYPTION_HAS_FAILED, e.getMessage()), e);
40+
}
41+
}
42+
43+
public static String decrypt(byte[] encryptedValue, byte[] encryptionKey) {
44+
try {
45+
byte[] gcmInitialisationVector = new byte[Constants.INITIALISATION_VECTOR_LENGTH];
46+
System.arraycopy(encryptedValue, 0, gcmInitialisationVector, 0,
47+
gcmInitialisationVector.length);
48+
49+
byte[] cipherValue = new byte[encryptedValue.length - Constants.INITIALISATION_VECTOR_LENGTH];
50+
System.arraycopy(encryptedValue, Constants.INITIALISATION_VECTOR_POSITION, cipherValue, 0, cipherValue.length);
51+
52+
Cipher cipherObject = setUpCipherObject(encryptionKey, gcmInitialisationVector, Cipher.DECRYPT_MODE);
53+
54+
byte[] resultInBytes = cipherObject.doFinal(cipherValue);
55+
return new String(resultInBytes, StandardCharsets.UTF_8);
56+
} catch (Exception e) {
57+
throw new SLException(MessageFormat.format(Messages.DECRYPTION_HAS_FAILED, e.getMessage()), e);
58+
}
59+
}
60+
61+
private static Cipher setUpCipherObject(byte[] encryptionKey, byte[] gcmInitialisationVector, int cipherMode)
62+
throws InvalidAlgorithmParameterException, InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException,
63+
NoSuchProviderException {
64+
Cipher cipherObject = Cipher.getInstance(Constants.CIPHER_TRANSFORMATION_NAME, BouncyCastleFipsProvider.PROVIDER_NAME);
65+
SecretKeySpec secretKeySpec = new SecretKeySpec(encryptionKey, Constants.ENCRYPTION_DECRYPTION_ALGORITHM_NAME);
66+
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(Constants.GCM_AUTHENTICATION_TAG_LENGTH, gcmInitialisationVector);
67+
68+
cipherObject.init(cipherMode, secretKeySpec, gcmParameterSpec);
69+
70+
return cipherObject;
71+
}
72+
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package org.cloudfoundry.multiapps.controller.core.security.serialization;
2+
3+
import org.cloudfoundry.multiapps.controller.core.security.serialization.model.DeploymentDescriptorSerializer;
4+
import org.cloudfoundry.multiapps.controller.core.security.serialization.model.ModuleSerializer;
5+
import org.cloudfoundry.multiapps.controller.core.security.serialization.model.ProvidedDependencySerializer;
6+
import org.cloudfoundry.multiapps.controller.core.security.serialization.model.RequiredDependencySerializer;
7+
import org.cloudfoundry.multiapps.controller.core.security.serialization.model.ResourceSerializer;
8+
import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor;
9+
import org.cloudfoundry.multiapps.mta.model.Module;
10+
import org.cloudfoundry.multiapps.mta.model.ProvidedDependency;
11+
import org.cloudfoundry.multiapps.mta.model.RequiredDependency;
12+
import org.cloudfoundry.multiapps.mta.model.Resource;
13+
import org.cloudfoundry.multiapps.mta.model.VersionedEntity;
14+
15+
public final class DynamicSecureSerialization {
16+
17+
private final SecureSerializerConfiguration secureSerializerConfiguration;
18+
19+
public DynamicSecureSerialization(SecureSerializerConfiguration secureSerializerConfiguration) {
20+
this.secureSerializerConfiguration = secureSerializerConfiguration;
21+
}
22+
23+
public String toJson(Object object) {
24+
SecureJsonSerializer secureJsonSerializer = createDynamicJsonSerializer(object);
25+
return secureJsonSerializer.serialize(object);
26+
}
27+
28+
private SecureJsonSerializer createDynamicJsonSerializer(Object object) {
29+
SecureJsonSerializer secureJsonSerializer = createDynamicJsonSerializerForVersionedEntity(object);
30+
if (secureJsonSerializer == null) {
31+
return new SecureJsonSerializer(secureSerializerConfiguration);
32+
}
33+
34+
return secureJsonSerializer;
35+
}
36+
37+
private SecureJsonSerializer createDynamicJsonSerializerForVersionedEntity(Object object) {
38+
if (object instanceof VersionedEntity) {
39+
return createDynamicJsonSerializerForVersionedEntity((VersionedEntity) object);
40+
}
41+
42+
return null;
43+
}
44+
45+
private SecureJsonSerializer createDynamicJsonSerializerForVersionedEntity(VersionedEntity versionedEntity) {
46+
if (versionedEntity.getMajorSchemaVersion() < 3) {
47+
return null;
48+
}
49+
50+
if (versionedEntity instanceof DeploymentDescriptor) {
51+
return new DeploymentDescriptorSerializer(secureSerializerConfiguration);
52+
}
53+
54+
if (versionedEntity instanceof Module) {
55+
return new ModuleSerializer(secureSerializerConfiguration);
56+
}
57+
58+
if (versionedEntity instanceof ProvidedDependency) {
59+
return new ProvidedDependencySerializer(secureSerializerConfiguration);
60+
}
61+
62+
if (versionedEntity instanceof RequiredDependency) {
63+
return new RequiredDependencySerializer(secureSerializerConfiguration);
64+
}
65+
66+
if (versionedEntity instanceof Resource) {
67+
return new ResourceSerializer(secureSerializerConfiguration);
68+
}
69+
70+
return null;
71+
}
72+
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.cloudfoundry.multiapps.controller.core.security.serialization;
2+
3+
import java.util.Collection;
4+
5+
public final class SecureSerializationFactory {
6+
7+
private SecureSerializationFactory() {
8+
9+
}
10+
11+
public static DynamicSecureSerialization ofAdditionalValues(Collection<String> additionalSensitiveElementNames) {
12+
SecureSerializerConfiguration secureSerializerConfigurationWithAdditionalValues = new SecureSerializerConfiguration();
13+
14+
secureSerializerConfigurationWithAdditionalValues.setAdditionalSensitiveElementNames(additionalSensitiveElementNames);
15+
return new DynamicSecureSerialization(secureSerializerConfigurationWithAdditionalValues);
16+
}
17+
18+
}

multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/security/serialization/SecureSerializerConfiguration.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
import java.util.Collection;
44
import java.util.Collections;
5+
import java.util.HashSet;
56
import java.util.List;
7+
import java.util.Set;
68

79
import org.apache.commons.lang3.StringUtils;
10+
import org.springframework.util.CollectionUtils;
811

912
public class SecureSerializerConfiguration {
1013

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

23+
private Collection<String> additionalSensitiveElementNames = Collections.emptyList();
24+
2025
public Collection<String> getSensitiveElementNames() {
21-
return sensitiveElementNames;
26+
if (CollectionUtils.isEmpty(additionalSensitiveElementNames)) {
27+
return sensitiveElementNames;
28+
}
29+
30+
Set<String> mergedSensitiveElementNames = new HashSet<>(sensitiveElementNames);
31+
32+
for (String additionalSensitiveElement : additionalSensitiveElementNames) {
33+
boolean isNotExistent = mergedSensitiveElementNames.stream()
34+
.noneMatch(sensitiveElement -> sensitiveElement.equalsIgnoreCase(
35+
additionalSensitiveElement));
36+
if (isNotExistent) {
37+
mergedSensitiveElementNames.add(additionalSensitiveElement);
38+
}
39+
}
40+
41+
return mergedSensitiveElementNames;
2242
}
2343

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

56+
public void setAdditionalSensitiveElementNames(Collection<String> additionalSensitiveElementNames) {
57+
this.additionalSensitiveElementNames = additionalSensitiveElementNames;
58+
}
59+
3660
public void setSensitiveElementPaths(Collection<String> sensitiveElementPaths) {
3761
this.sensitiveElementPaths = sensitiveElementPaths;
3862
}

0 commit comments

Comments
 (0)