Skip to content

Commit

Permalink
Encrypt & decrypt build info properties file (#766)
Browse files Browse the repository at this point in the history
  • Loading branch information
Or-Geva authored Dec 3, 2023
1 parent dc03705 commit ca03eac
Show file tree
Hide file tree
Showing 10 changed files with 524 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
import org.jfrog.build.extractor.clientConfiguration.ClientProperties;
import org.jfrog.build.extractor.clientConfiguration.IncludeExcludePatterns;
import org.jfrog.build.extractor.clientConfiguration.PatternMatcher;
import org.jfrog.build.extractor.clientConfiguration.util.encryption.EncryptionKeyPair;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -27,6 +31,9 @@
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
Expand All @@ -39,6 +46,7 @@
import static org.apache.commons.lang3.StringUtils.removeEnd;
import static org.jfrog.build.extractor.UrlUtils.encodeUrl;
import static org.jfrog.build.extractor.UrlUtils.encodeUrlPathPart;
import static org.jfrog.build.extractor.clientConfiguration.util.encryption.PropertyEncryptor.decryptPropertiesFromFile;

/**
* @author Noam Y. Tenne
Expand All @@ -64,33 +72,57 @@ public abstract class BuildInfoExtractorUtils {
public static final Predicate<Object> MATRIX_PARAM_PREDICATE =
new PrefixPredicate(ClientProperties.PROP_DEPLOY_PARAM_PROP_PREFIX);

public static Properties mergePropertiesWithSystemAndPropertyFile(Properties existingProps) {
return mergePropertiesWithSystemAndPropertyFile(existingProps, null);
public static Properties mergePropertiesWithSystemAndPropertyFile(Properties existingProps, Log log) {
Properties mergedProps = new Properties();
mergedProps.putAll(addSystemProperties(existingProps));
mergedProps.putAll(searchAdditionalPropertiesFile(mergedProps, log));
return mergedProps;
}

public static Properties mergePropertiesWithSystemAndPropertyFile(Properties existingProps, Log log) {

private static Properties addSystemProperties(Properties existingProps) {
Properties props = new Properties();
props.putAll(existingProps);
props.putAll(System.getProperties());
return props;
}

/**
* Retrieves additional properties from a specified build info properties file path.
*
* @param existingProps Existing properties object.
* @param log Logger instance for logging debug information.
* @return Properties object containing additional properties if found; otherwise, an empty properties object.
*/
private static Properties searchAdditionalPropertiesFile(Properties existingProps, Log log) {
Properties props = new Properties();
addPropsFromCommandSystemProp(existingProps, log);
String propsFilePath = getAdditionalPropertiesFile(existingProps, log);
if (StringUtils.isNotBlank(propsFilePath)) {
File propertiesFile = new File(propsFilePath);
InputStream inputStream = null;
try {
if (propertiesFile.exists()) {
inputStream = Files.newInputStream(propertiesFile.toPath());

if (StringUtils.isBlank(propsFilePath)) {
log.debug("[buildinfo] BuildInfo properties file path is not found.");
return props;
}

File propertiesFile = new File(propsFilePath);
if (!propertiesFile.exists()) {
log.debug("[buildinfo] BuildInfo properties file is not exists.");
return props;
}

try {
EncryptionKeyPair keyPair = new EncryptionKeyPair(getPropertiesFileEncryptionKey(existingProps), getPropertiesFileEncryptionKeyIv(existingProps));
if (!keyPair.isEmpty()) {
log.debug("[buildinfo] Found an encryption for buildInfo properties file for this build.");
props.putAll(decryptPropertiesFromFile(propertiesFile.getPath(), keyPair));
} else {
try (InputStream inputStream = Files.newInputStream(propertiesFile.toPath())) {
props.load(inputStream);
}
} catch (IOException e) {
throw new RuntimeException(
"Unable to load build info properties from file: " + propertiesFile.getAbsolutePath(), e);
} finally {
IOUtils.closeQuietly(inputStream);
}
} catch (IOException | InvalidAlgorithmParameterException | IllegalBlockSizeException | NoSuchPaddingException |
BadPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Unable to load build info properties from file: " + propertiesFile.getAbsolutePath(), e);
}

props.putAll(existingProps);
props.putAll(System.getProperties());

return props;
}

Expand Down Expand Up @@ -237,11 +269,45 @@ public static void saveBuildInfoToFile(BuildInfo buildInfo, File toFile) throws
CommonUtils.writeByCharset(buildInfoJson, toFile, StandardCharsets.UTF_8);
}

/**
* @param additionalProps Additional properties containing the encryption key.
* @return The encryption key obtained from system properties or additional properties.
*/
private static String getPropertiesFileEncryptionKey(Properties additionalProps) {
return getPropertiesFileEncryption(additionalProps, BuildInfoConfigProperties.PROP_PROPS_FILE_KEY);
}

/**
* @param additionalProps Additional properties containing the encryption IV.
* @return The encryption IV obtained from system properties or additional properties.
*/
private static String getPropertiesFileEncryptionKeyIv(Properties additionalProps) {
return getPropertiesFileEncryption(additionalProps, BuildInfoConfigProperties.PROP_PROPS_FILE_KEY_IV);
}

private static String getPropertiesFileEncryption(Properties additionalProps, String key) {
// Check if the encryption key is set in system properties
if (StringUtils.isNotBlank(System.getProperty(key))) {
return System.getProperty(key);
}
if (additionalProps != null) {
// Check for the encryption key directly in additional properties
if (StringUtils.isNotBlank(additionalProps.getProperty(key))) {
return additionalProps.getProperty(key);
}
// Jenkins prefixes these variables with "env." so let's try that
if (StringUtils.isNotBlank(additionalProps.getProperty("env." + key))) {
return additionalProps.getProperty("env." + key);
}
}
return null;
}

private static String getAdditionalPropertiesFile(Properties additionalProps, Log log) {
String key = BuildInfoConfigProperties.PROP_PROPS_FILE;
String filePath = System.getProperty(key);
String propFoundPath = "System.getProperty(" + key + ")";
if (StringUtils.isBlank(filePath) && additionalProps != null) {
if (StringUtils.isBlank(filePath)) {
filePath = additionalProps.getProperty(key);
propFoundPath = "additionalProps.getProperty(" + key + ")";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ public interface BuildInfoConfigProperties {
*/
String BUILD_INFO_CONFIG_PREFIX = "buildInfoConfig.";
String PROPERTIES_FILE = "propertiesFile";
String PROPERTIES_FILE_KEY = "propertiesFileKey";
String PROPERTIES_FILE_KEY_IV = "propertiesFileKeyIv";

String PROP_PROPS_FILE = BUILD_INFO_CONFIG_PREFIX + PROPERTIES_FILE;
String PROP_PROPS_FILE_KEY = BUILD_INFO_CONFIG_PREFIX + PROPERTIES_FILE_KEY;
String PROP_PROPS_FILE_KEY_IV = BUILD_INFO_CONFIG_PREFIX + PROPERTIES_FILE_KEY_IV;

String EXPORT_FILE = "exportFile";
String PROP_EXPORT_FILE_PATH = BUILD_INFO_CONFIG_PREFIX + EXPORT_FILE;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jfrog.build.api.util.CommonUtils;
import org.jfrog.build.api.util.Log;
import org.jfrog.build.extractor.ci.BuildInfo;
import org.jfrog.build.extractor.ci.BuildInfoFields;
import org.jfrog.build.extractor.ci.Issue;
import org.jfrog.build.extractor.clientConfiguration.util.IssuesTrackerUtils;
import org.jfrog.build.extractor.clientConfiguration.util.encryption.EncryptionKeyPair;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
Expand Down Expand Up @@ -136,6 +143,7 @@
import static org.jfrog.build.extractor.clientConfiguration.ClientProperties.PROP_RESOLVE_PREFIX;
import static org.jfrog.build.extractor.clientConfiguration.ClientProperties.PROP_SO_TIMEOUT;
import static org.jfrog.build.extractor.clientConfiguration.ClientProperties.PROP_TIMEOUT;
import static org.jfrog.build.extractor.clientConfiguration.util.encryption.PropertyEncryptor.encryptedPropertiesToStream;

/**
* @author freds
Expand Down Expand Up @@ -230,20 +238,38 @@ public void persistToPropertiesFile() {
if (StringUtils.isEmpty(getPropertiesFile())) {
return;
}
Properties props = new Properties();
props.putAll(filterMapNullValues(root.props));
props.putAll(filterMapNullValues(rootConfig.props));
FileOutputStream fos = null;
try {
fos = new FileOutputStream(new File(getPropertiesFile()).getCanonicalFile());
props.store(fos, "BuildInfo configuration property file");
try (FileOutputStream fos = new FileOutputStream(new File(getPropertiesFile()).getCanonicalFile())) {
preparePropertiesToPersist().store(fos, "BuildInfo configuration property file");
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
IOUtils.closeQuietly(fos);
}
}


public EncryptionKeyPair persistToEncryptedPropertiesFile(OutputStream os) throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
if (StringUtils.isEmpty(getPropertiesFile())) {
return null;
}
return encryptedPropertiesToStream(os, preparePropertiesToPersist());
}

private Properties preparePropertiesToPersist() {
Properties props = new Properties();
props.putAll(filterMapNullValues(rootConfig.props));
props.putAll(filterMapNullValues(root.props));

// Properties that have the 'artifactory.' prefix are deprecated.
// For backward compatibility reasons, both will be added to the props map.
// The build-info 2.32.3 is the last version to be using the deprecated properties as its primary.
Properties artifactoryProps = new Properties();
for (String key : props.stringPropertyNames()) {
artifactoryProps.put("artifactory." + key, props.getProperty(key));
}
props.putAll(artifactoryProps);

return props;
}

public static Map<String, String> filterMapNullValues(Map<String, String> map) {
Map<String, String> result = new HashMap<String, String>();
for (Map.Entry<String, String> entry : map.entrySet()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.jfrog.build.extractor.clientConfiguration.util.encryption;

import org.apache.commons.lang3.StringUtils;

import java.security.SecureRandom;
import java.util.Base64;

/**
* Represents a pair of secret key and initialization vector (IV) used for encryption and decryption.
*/
public class EncryptionKeyPair {
private static final int AES_256_KEY_LENGTH = 256;
private static final int IV_LENGTH = 128;
private byte[] secretKey;
private byte[] iv;

public EncryptionKeyPair() {
this.secretKey = generateRandomKey(AES_256_KEY_LENGTH);
this.iv = generateRandomKey(IV_LENGTH);
}

public EncryptionKeyPair(String secretKey, String Iv) {
if (StringUtils.isNotBlank(secretKey)) {
this.secretKey = Base64.getDecoder().decode(secretKey);
}
if (StringUtils.isNotBlank(Iv)) {
this.iv = Base64.getDecoder().decode(Iv);
}
}

/**
* Generates a random key of the specified length in bits.
*
* @param lengthInBits The length of the key in bits.
* @return A byte array representing the generated random key.
*/
private static byte[] generateRandomKey(int lengthInBits) {
SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[lengthInBits / 8];
secureRandom.nextBytes(key);
return key;
}

public byte[] getSecretKey() {
return secretKey;
}

@SuppressWarnings("unused")
public String getStringSecretKey() {
return Base64.getEncoder().encodeToString(secretKey);
}

public byte[] getIv() {
return iv;
}

@SuppressWarnings("unused")
public String getStringIv() {
return Base64.getEncoder().encodeToString(iv);
}

public boolean isEmpty() {
return secretKey == null || secretKey.length == 0 || iv == null || iv.length == 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.jfrog.build.extractor.clientConfiguration.util.encryption;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class Encryptor {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128;

/**
* Decrypts the given data using the provided EncryptionKeyPair.
*
* @param data The encrypted data to be decrypted
* @param keyPair The EncryptionKeyPair containing the secret key and IV for decryption
* @return The decrypted data as a byte array
*/
public static byte[] decrypt(byte[] data, EncryptionKeyPair keyPair) throws IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyPair.getSecretKey(), ALGORITHM);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, keyPair.getIv());
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
return cipher.doFinal(data);
}

/**
* Encrypts the given data using the provided EncryptionKeyPair.
*
* @param data The data to be encrypted
* @param keyPair The EncryptionKeyPair containing the secret key and IV for encryption
* @return The encrypted data as a byte array
*/
public static byte[] encrypt(byte[] data, EncryptionKeyPair keyPair) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyPair.getSecretKey(), ALGORITHM);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, keyPair.getIv());
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec);
return cipher.doFinal(data);
}
}
Loading

0 comments on commit ca03eac

Please sign in to comment.