From ca03eac35a63a667947be9fac2f7070290e3114e Mon Sep 17 00:00:00 2001 From: Or Geva Date: Sun, 3 Dec 2023 10:03:03 +0200 Subject: [PATCH] Encrypt & decrypt build info properties file (#766) --- .../extractor/BuildInfoExtractorUtils.java | 106 ++++++++++++++---- .../ci/BuildInfoConfigProperties.java | 6 + .../ArtifactoryClientConfiguration.java | 46 ++++++-- .../util/encryption/EncryptionKeyPair.java | 65 +++++++++++ .../util/encryption/Encryptor.java | 47 ++++++++ .../util/encryption/PropertyEncryptor.java | 83 ++++++++++++++ .../extractor/BuildExtractorUtilsTest.java | 76 +++++++++++-- .../ArtifactoryClientConfigurationTest.java | 46 ++++++++ .../util/encryption/EncryptorTest.java | 34 ++++++ .../encryption/PropertyEncryptorTest.java | 56 +++++++++ 10 files changed, 524 insertions(+), 41 deletions(-) create mode 100644 build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/util/encryption/EncryptionKeyPair.java create mode 100644 build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/util/encryption/Encryptor.java create mode 100644 build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/util/encryption/PropertyEncryptor.java create mode 100644 build-info-extractor/src/test/java/org/jfrog/build/extractor/util/encryption/EncryptorTest.java create mode 100644 build-info-extractor/src/test/java/org/jfrog/build/extractor/util/encryption/PropertyEncryptorTest.java diff --git a/build-info-extractor/src/main/java/org/jfrog/build/extractor/BuildInfoExtractorUtils.java b/build-info-extractor/src/main/java/org/jfrog/build/extractor/BuildInfoExtractorUtils.java index 11609d149..8f6de57d3 100644 --- a/build-info-extractor/src/main/java/org/jfrog/build/extractor/BuildInfoExtractorUtils.java +++ b/build-info-extractor/src/main/java/org/jfrog/build/extractor/BuildInfoExtractorUtils.java @@ -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; @@ -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; @@ -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 @@ -64,33 +72,57 @@ public abstract class BuildInfoExtractorUtils { public static final Predicate 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; } @@ -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 + ")"; } diff --git a/build-info-extractor/src/main/java/org/jfrog/build/extractor/ci/BuildInfoConfigProperties.java b/build-info-extractor/src/main/java/org/jfrog/build/extractor/ci/BuildInfoConfigProperties.java index f24bd1b6a..fda2b4828 100644 --- a/build-info-extractor/src/main/java/org/jfrog/build/extractor/ci/BuildInfoConfigProperties.java +++ b/build-info-extractor/src/main/java/org/jfrog/build/extractor/ci/BuildInfoConfigProperties.java @@ -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; diff --git a/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/ArtifactoryClientConfiguration.java b/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/ArtifactoryClientConfiguration.java index fb1d4b571..2bb90e989 100644 --- a/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/ArtifactoryClientConfiguration.java +++ b/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/ArtifactoryClientConfiguration.java @@ -2,7 +2,6 @@ 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; @@ -10,12 +9,20 @@ 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; @@ -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 @@ -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 filterMapNullValues(Map map) { Map result = new HashMap(); for (Map.Entry entry : map.entrySet()) { diff --git a/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/util/encryption/EncryptionKeyPair.java b/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/util/encryption/EncryptionKeyPair.java new file mode 100644 index 000000000..7de47aeb6 --- /dev/null +++ b/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/util/encryption/EncryptionKeyPair.java @@ -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; + } +} diff --git a/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/util/encryption/Encryptor.java b/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/util/encryption/Encryptor.java new file mode 100644 index 000000000..f86e88861 --- /dev/null +++ b/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/util/encryption/Encryptor.java @@ -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); + } +} diff --git a/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/util/encryption/PropertyEncryptor.java b/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/util/encryption/PropertyEncryptor.java new file mode 100644 index 000000000..a04cce87e --- /dev/null +++ b/build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/util/encryption/PropertyEncryptor.java @@ -0,0 +1,83 @@ +package org.jfrog.build.extractor.clientConfiguration.util.encryption; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Properties; + +import static org.jfrog.build.extractor.clientConfiguration.util.encryption.Encryptor.decrypt; +import static org.jfrog.build.extractor.clientConfiguration.util.encryption.Encryptor.encrypt; + +public class PropertyEncryptor { + + /** + * Decrypts properties from a file using the provided secret key. + * + * @param filePath The path to the file containing encrypted properties. + * @param keyPair The secret key and iv used for decryption. + * @return A Properties object containing the decrypted properties. + */ + public static Properties decryptPropertiesFromFile(String filePath, EncryptionKeyPair keyPair) throws IOException, InvalidAlgorithmParameterException, IllegalBlockSizeException, NoSuchPaddingException, BadPaddingException, NoSuchAlgorithmException, InvalidKeyException { + if (StringUtils.isBlank(filePath)) { + return null; + } + if (!new File(filePath).exists()) { + throw new IOException("File " + filePath + " does not exist"); + } + return decryptProperties(FileUtils.readFileToByteArray(new File(filePath)), keyPair); + } + + /** + * Decrypts properties from an encrypted byte array using the provided secret key. + * + * @param encryptedData The encrypted byte array representing properties. + * @param keyPair The secret key and iv used for decryption. + * @return A Properties object containing the decrypted properties. + */ + private static Properties decryptProperties(byte[] encryptedData, EncryptionKeyPair keyPair) throws IOException, InvalidAlgorithmParameterException, IllegalBlockSizeException, NoSuchPaddingException, BadPaddingException, NoSuchAlgorithmException, InvalidKeyException { + byte[] decryptedBytes = decrypt(encryptedData, keyPair); + String decryptedString = new String(decryptedBytes, StandardCharsets.UTF_8); + return stringToProperties(decryptedString); + } + + /** + * Encrypts properties to a file represented as a byte array and returns the secret key used for encryption. + * + * @param os The output stream where the encrypted properties will be written. + * @param properties The Properties object containing the properties to be encrypted. + * @return A byte array representing the secret key used for encryption. + */ + public static EncryptionKeyPair encryptedPropertiesToStream(OutputStream os, Properties properties) throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + EncryptionKeyPair keyPair = new EncryptionKeyPair(); + os.write(encrypt(propertiesToString(properties).getBytes(StandardCharsets.UTF_8), keyPair)); + return keyPair; + } + + private static Properties stringToProperties(String propertiesString) throws IOException { + Properties properties = new Properties(); + try (InputStream inputStream = new ByteArrayInputStream(propertiesString.getBytes(StandardCharsets.UTF_8))) { + properties.load(inputStream); + } + return properties; + } + + private static String propertiesToString(Properties properties) { + StringBuilder stringBuilder = new StringBuilder(); + for (String key : properties.stringPropertyNames()) { + stringBuilder.append(key).append("=").append(properties.getProperty(key)).append("\n"); + } + return stringBuilder.toString(); + } +} diff --git a/build-info-extractor/src/test/java/org/jfrog/build/extractor/BuildExtractorUtilsTest.java b/build-info-extractor/src/test/java/org/jfrog/build/extractor/BuildExtractorUtilsTest.java index 0fc05cd88..d81606a8c 100644 --- a/build-info-extractor/src/test/java/org/jfrog/build/extractor/BuildExtractorUtilsTest.java +++ b/build-info-extractor/src/test/java/org/jfrog/build/extractor/BuildExtractorUtilsTest.java @@ -1,24 +1,46 @@ package org.jfrog.build.extractor; import org.apache.commons.lang3.ArrayUtils; +import org.jfrog.build.api.util.NullLog; import org.jfrog.build.extractor.builder.BuildInfoBuilder; import org.jfrog.build.extractor.builder.DependencyBuilder; import org.jfrog.build.extractor.builder.ModuleBuilder; -import org.jfrog.build.extractor.ci.*; +import org.jfrog.build.extractor.ci.BuildInfo; +import org.jfrog.build.extractor.ci.BuildInfoConfigProperties; +import org.jfrog.build.extractor.ci.BuildInfoProperties; +import org.jfrog.build.extractor.ci.Dependency; import org.jfrog.build.extractor.ci.Module; +import org.jfrog.build.extractor.clientConfiguration.ArtifactoryClientConfiguration; +import org.jfrog.build.extractor.clientConfiguration.util.encryption.EncryptionKeyPair; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; import java.util.Properties; -import static org.jfrog.build.extractor.BuildInfoExtractorUtils.*; -import static org.testng.Assert.*; +import static org.jfrog.build.IntegrationTestsBase.getLog; +import static org.jfrog.build.extractor.BuildInfoExtractorUtils.BUILD_INFO_PROP_PREDICATE; +import static org.jfrog.build.extractor.BuildInfoExtractorUtils.buildInfoToJsonString; +import static org.jfrog.build.extractor.BuildInfoExtractorUtils.createBuildInfoUrl; +import static org.jfrog.build.extractor.BuildInfoExtractorUtils.filterDynamicProperties; +import static org.jfrog.build.extractor.BuildInfoExtractorUtils.getEnvProperties; +import static org.jfrog.build.extractor.BuildInfoExtractorUtils.jsonStringToBuildInfo; +import static org.jfrog.build.extractor.BuildInfoExtractorUtils.mergePropertiesWithSystemAndPropertyFile; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; /** * Test the build info extractor @@ -42,13 +64,17 @@ private void setUp() throws IOException { @AfterMethod private void tearDown() throws IOException { Files.deleteIfExists(tempFile); + + System.clearProperty(BuildInfoConfigProperties.PROP_PROPS_FILE); + System.clearProperty(BuildInfoConfigProperties.PROP_PROPS_FILE_KEY); + System.clearProperty(BuildInfoConfigProperties.PROP_PROPS_FILE_KEY_IV); } public void getBuildInfoPropertiesFromSystemProps() { System.setProperty(POPO_KEY, "buildname"); System.setProperty(MOMO_KEY, "1"); - Properties props = filterDynamicProperties(mergePropertiesWithSystemAndPropertyFile(new Properties()), BUILD_INFO_PROP_PREDICATE); + Properties props = filterDynamicProperties(mergePropertiesWithSystemAndPropertyFile(new Properties(), getLog()), BUILD_INFO_PROP_PREDICATE); assertEquals(props.size(), 2, "there should only be 2 properties after the filtering"); assertEquals(props.getProperty(POPO_KEY), "buildname", "popo property does not match"); @@ -68,14 +94,46 @@ public void getBuildInfoPropertiesFromFile() throws IOException { System.setProperty(BuildInfoConfigProperties.PROP_PROPS_FILE, tempFile.toString()); Properties fileProps = filterDynamicProperties( - mergePropertiesWithSystemAndPropertyFile(new Properties()), + mergePropertiesWithSystemAndPropertyFile(new Properties(), getLog()), BUILD_INFO_PROP_PREDICATE); assertEquals(fileProps.size(), 2, "there should only be 2 properties after the filtering"); assertEquals(fileProps.getProperty(POPO_KEY), "buildname", "popo property does not match"); assertEquals(fileProps.getProperty(MOMO_KEY), "1", "momo property does not match"); + } - System.clearProperty(BuildInfoConfigProperties.PROP_PROPS_FILE); + public void getBuildInfoPropertiesFromEncryptedFile() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + EncryptionKeyPair keyPair = setupEncryptedFileTest(); + System.setProperty(BuildInfoConfigProperties.PROP_PROPS_FILE_KEY, Base64.getEncoder().encodeToString(keyPair.getSecretKey())); + System.setProperty(BuildInfoConfigProperties.PROP_PROPS_FILE_KEY_IV, Base64.getEncoder().encodeToString(keyPair.getIv())); + Properties fileProps = filterDynamicProperties( + mergePropertiesWithSystemAndPropertyFile(new Properties(), getLog()), + BUILD_INFO_PROP_PREDICATE); + assertEquals(fileProps.size(), 2, "there should only be 2 properties after the filtering"); + assertEquals(fileProps.getProperty(POPO_KEY), "buildname", "popo property does not match"); + assertEquals(fileProps.getProperty(MOMO_KEY), "1", "momo property does not match"); + } + + public void failToReadEncryptedFileWithNoKey() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + setupEncryptedFileTest(); + Properties fileProps = filterDynamicProperties( + mergePropertiesWithSystemAndPropertyFile(new Properties(), getLog()), + BUILD_INFO_PROP_PREDICATE); + assertEquals(fileProps.size(), 0, "0 properties should be present, the file is encrypted, and the key is not available"); + } + + private EncryptionKeyPair setupEncryptedFileTest() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + Properties props = new Properties(); + props.put(POPO_KEY, "buildname"); + props.put(MOMO_KEY, "1"); + props.put(BuildInfoConfigProperties.PROP_PROPS_FILE, tempFile.toString()); + System.setProperty(BuildInfoConfigProperties.PROP_PROPS_FILE, tempFile.toString()); + ArtifactoryClientConfiguration client = new ArtifactoryClientConfiguration(new NullLog()); + client.fillFromProperties(props); + + try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile.toFile())) { + return client.persistToEncryptedPropertiesFile(fileOutputStream); + } } public void getBuildInfoProperties() throws IOException { @@ -94,7 +152,7 @@ public void getBuildInfoProperties() throws IOException { System.setProperty(gogoKey, "2"); Properties buildInfoProperties = filterDynamicProperties( - mergePropertiesWithSystemAndPropertyFile(new Properties()), + mergePropertiesWithSystemAndPropertyFile(new Properties(), getLog()), BUILD_INFO_PROP_PREDICATE); assertEquals(buildInfoProperties.size(), 4, "There should be 4 properties"); @@ -103,7 +161,6 @@ public void getBuildInfoProperties() throws IOException { assertEquals(buildInfoProperties.getProperty(kokoKey), "parent", "koko parent name property does not match"); assertEquals(buildInfoProperties.getProperty(gogoKey), "2", "gogo parent number property does not match"); - System.clearProperty(BuildInfoConfigProperties.PROP_PROPS_FILE); System.clearProperty(kokoKey); System.clearProperty(gogoKey); } @@ -120,8 +177,6 @@ public void getEnvPropertiesFromFile() throws IOException { Properties fileProps = getEnvProperties(new Properties(), null); assertEquals(fileProps.getProperty(ENV_POPO_KEY), "buildname", "popo property does not match"); assertEquals(fileProps.getProperty(ENV_MOMO_KEY), "1", "momo property does not match"); - - System.clearProperty(BuildInfoConfigProperties.PROP_PROPS_FILE); } public void getEnvAndSysPropertiesFromFile() throws IOException { @@ -145,7 +200,6 @@ public void getEnvAndSysPropertiesFromFile() throws IOException { assertEquals(buildInfoProperties.getProperty("koko"), "parent", "koko parent name property does not match"); assertEquals(buildInfoProperties.getProperty("gogo"), "2", "gogo parent number property does not match"); - System.clearProperty(BuildInfoConfigProperties.PROP_PROPS_FILE); System.clearProperty(kokoKey); System.clearProperty(gogoKey); } diff --git a/build-info-extractor/src/test/java/org/jfrog/build/extractor/clientConfiguration/ArtifactoryClientConfigurationTest.java b/build-info-extractor/src/test/java/org/jfrog/build/extractor/clientConfiguration/ArtifactoryClientConfigurationTest.java index 45d75c463..832c9a4a0 100644 --- a/build-info-extractor/src/test/java/org/jfrog/build/extractor/clientConfiguration/ArtifactoryClientConfigurationTest.java +++ b/build-info-extractor/src/test/java/org/jfrog/build/extractor/clientConfiguration/ArtifactoryClientConfigurationTest.java @@ -1,15 +1,33 @@ package org.jfrog.build.extractor.clientConfiguration; import org.jfrog.build.api.util.NullLog; +import org.jfrog.build.extractor.ci.BuildInfoConfigProperties; +import org.jfrog.build.extractor.clientConfiguration.util.encryption.EncryptionKeyPair; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.Properties; +import static org.jfrog.build.extractor.clientConfiguration.util.encryption.PropertyEncryptor.decryptPropertiesFromFile; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; @Test public class ArtifactoryClientConfigurationTest { + private Path tempFile; + @Test(description = "Test http and https proxy handler.") public void testProxyHandler() { ArtifactoryClientConfiguration client = createProxyClient(); @@ -29,6 +47,34 @@ public void testProxyHandler() { assertNull(httpsProxy.getPassword()); } + @BeforeMethod + private void setUp() throws IOException { + tempFile = Files.createTempFile("BuildInfoExtractorUtilsTest", "").toAbsolutePath(); + } + + @AfterMethod + private void tearDown() throws IOException { + Files.deleteIfExists(tempFile); + } + + @Test(description = "Test read encrypted property file") + public void testReadEncryptedPropertyFile() throws IOException, InvalidAlgorithmParameterException, IllegalBlockSizeException, NoSuchPaddingException, BadPaddingException, NoSuchAlgorithmException, InvalidKeyException { + // Prepare + ArtifactoryClientConfiguration client = createProxyClient(); + tempFile = Files.createTempFile("BuildInfoExtractorUtilsTest", "").toAbsolutePath(); + client.root.props.put(BuildInfoConfigProperties.PROP_PROPS_FILE, tempFile.toString()); + + try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile.toFile())) { + // Save encrypted file. + EncryptionKeyPair keyPair = client.persistToEncryptedPropertiesFile(fileOutputStream); + // Assert decrypted successfully. + Properties props = decryptPropertiesFromFile(tempFile.toString(), keyPair); + assertNotNull(props); + assertEquals(props.size(), 18); + assertEquals(props.getProperty("proxy.host"), client.getAllProperties().get("proxy.host")); + } + } + private ArtifactoryClientConfiguration createProxyClient() { ArtifactoryClientConfiguration client = new ArtifactoryClientConfiguration(new NullLog()); Properties props = new Properties(); diff --git a/build-info-extractor/src/test/java/org/jfrog/build/extractor/util/encryption/EncryptorTest.java b/build-info-extractor/src/test/java/org/jfrog/build/extractor/util/encryption/EncryptorTest.java new file mode 100644 index 000000000..a00c0d6f0 --- /dev/null +++ b/build-info-extractor/src/test/java/org/jfrog/build/extractor/util/encryption/EncryptorTest.java @@ -0,0 +1,34 @@ +package org.jfrog.build.extractor.util.encryption; + +import org.jfrog.build.extractor.clientConfiguration.util.encryption.EncryptionKeyPair; +import org.jfrog.build.extractor.clientConfiguration.util.encryption.Encryptor; +import org.testng.annotations.Test; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import static org.testng.AssertJUnit.assertEquals; + +public class EncryptorTest { + @Test + public void testEncryptionDecryption() throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + EncryptionKeyPair keyPair = new EncryptionKeyPair(); + // Data to be encrypted + String originalData = "This is a secret message"; + byte[] dataToEncrypt = originalData.getBytes(); + + // Encrypt the data + byte[] encryptedData = Encryptor.encrypt(dataToEncrypt, keyPair); + + // Decrypt the data + byte[] decryptedData = Encryptor.decrypt(encryptedData, keyPair); + + // Verify if decrypted data matches the original data + String decryptedMessage = new String(decryptedData); + assertEquals(originalData, decryptedMessage); + } +} diff --git a/build-info-extractor/src/test/java/org/jfrog/build/extractor/util/encryption/PropertyEncryptorTest.java b/build-info-extractor/src/test/java/org/jfrog/build/extractor/util/encryption/PropertyEncryptorTest.java new file mode 100644 index 000000000..32fabfd86 --- /dev/null +++ b/build-info-extractor/src/test/java/org/jfrog/build/extractor/util/encryption/PropertyEncryptorTest.java @@ -0,0 +1,56 @@ +package org.jfrog.build.extractor.util.encryption; + +import org.jfrog.build.extractor.clientConfiguration.util.encryption.EncryptionKeyPair; +import org.jfrog.build.extractor.clientConfiguration.util.encryption.PropertyEncryptor; +import org.testng.annotations.Test; + +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.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Properties; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +public class PropertyEncryptorTest { + + @Test + public void testDecryptPropertiesFromFile() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + // Create a temporary file with encrypted properties + Path tempFile = Files.createTempFile("encrypted_properties", ".properties"); + File file = tempFile.toFile(); + + Properties properties = new Properties(); + properties.setProperty("key1", "value1"); + properties.setProperty("key2", "value2"); + EncryptionKeyPair keyPair; + try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { + // Encrypt properties and write to the file + keyPair = PropertyEncryptor.encryptedPropertiesToStream(fileOutputStream, properties); + } + + String encryptedFilePath = file.getAbsolutePath(); + + try { + // Decrypt properties from the encrypted file + Properties decryptedProperties = PropertyEncryptor.decryptPropertiesFromFile(encryptedFilePath, keyPair); + + // Check if decrypted properties are as expected + assertNotNull(decryptedProperties); + assertEquals("value1", decryptedProperties.getProperty("key1"), "Decrypted property 'key1' should match"); + assertEquals("value2", decryptedProperties.getProperty("key2"), "Decrypted property 'key2' should match"); + } finally { + // Clean up - delete the temporary file + Files.deleteIfExists(Paths.get(encryptedFilePath)); + } + } +}