| /* |
| * Copyright (C) 2015, Andrei Pozolotin. and others |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Distribution License v. 1.0 which is available at |
| * https://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: BSD-3-Clause |
| */ |
| |
| package org.eclipse.jgit.transport; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.cryptoCipherListPBE; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.cryptoCipherListTrans; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.folderDelete; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.permitLongTests; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.policySetup; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.product; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.proxySetup; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.publicAddress; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.reportPolicy; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.securityProviderName; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.textWrite; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.transferStream; |
| import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.verifyFileContent; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assume.assumeTrue; |
| |
| import java.io.BufferedReader; |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.io.PrintWriter; |
| import java.net.SocketTimeoutException; |
| import java.net.URL; |
| import java.net.URLConnection; |
| import java.net.UnknownHostException; |
| import java.nio.file.Files; |
| import java.security.GeneralSecurityException; |
| import java.security.Provider; |
| import java.security.Security; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.UUID; |
| |
| import javax.crypto.SecretKeyFactory; |
| |
| import org.eclipse.jgit.api.Git; |
| import org.eclipse.jgit.lib.StoredConfig; |
| import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase; |
| import org.eclipse.jgit.util.FileUtils; |
| import org.junit.After; |
| import org.junit.AfterClass; |
| import org.junit.Before; |
| import org.junit.BeforeClass; |
| import org.junit.FixMethodOrder; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.MethodSorters; |
| import org.junit.runners.Parameterized; |
| import org.junit.runners.Parameterized.Parameters; |
| import org.junit.runners.Suite; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Amazon S3 encryption pipeline test. |
| * |
| * See {@link AmazonS3} {@link WalkEncryption} |
| * |
| * Note: CI server must provide amazon credentials (access key, secret key, |
| * bucket name) via one of methods available in {@link Names}. |
| * |
| * Note: long running tests are activated by Maven profile "test.long". There is |
| * also a separate Eclipse m2e launcher for that. See 'pom.xml' and |
| * 'WalkEncryptionTest.launch'. |
| */ |
| @RunWith(Suite.class) |
| @Suite.SuiteClasses({ // |
| WalkEncryptionTest.Required.class, // |
| WalkEncryptionTest.MinimalSet.class, // |
| WalkEncryptionTest.TestablePBE.class, // |
| WalkEncryptionTest.TestableTransformation.class, // |
| }) |
| public class WalkEncryptionTest { |
| |
| /** |
| * Logger setup: ${project_loc}/tst-rsrc/log4j.properties |
| */ |
| static final Logger logger = LoggerFactory.getLogger(WalkEncryptionTest.class); |
| |
| /** |
| * Property names used in test session. |
| */ |
| interface Names { |
| |
| // Names of discovered test properties. |
| |
| String TEST_BUCKET = "test.bucket"; |
| |
| // Names of test environment variables for CI. |
| |
| String ENV_ACCESS_KEY = "JGIT_S3_ACCESS_KEY"; |
| |
| String ENV_SECRET_KEY = "JGIT_S3_SECRET_KEY"; |
| |
| String ENV_BUCKET_NAME = "JGIT_S3_BUCKET_NAME"; |
| |
| // Name of test environment variable file path for CI. |
| |
| String ENV_CONFIG_FILE = "JGIT_S3_CONFIG_FILE"; |
| |
| // Names of test system properties for CI. |
| |
| String SYS_ACCESS_KEY = "jgit.s3.access.key"; |
| |
| String SYS_SECRET_KEY = "jgit.s3.secret.key"; |
| |
| String SYS_BUCKET_NAME = "jgit.s3.bucket.name"; |
| |
| // Name of test system property file path for CI. |
| String SYS_CONFIG_FILE = "jgit.s3.config.file"; |
| |
| // Hard coded name of test properties file for CI. |
| // File format follows AmazonS3.Keys: |
| // # |
| // # Required entries: |
| // # |
| // accesskey = your-amazon-access-key # default AmazonS3.Keys |
| // secretkey = your-amazon-secret-key # default AmazonS3.Keys |
| // test.bucket = your-bucket-for-testing # custom name, for this test |
| String CONFIG_FILE = "jgit-s3-config.properties"; |
| |
| // Test properties file in [user home] of CI. |
| String HOME_CONFIG_FILE = System.getProperty("user.home") |
| + File.separator + CONFIG_FILE; |
| |
| // Test properties file in [project work directory] of CI. |
| String WORK_CONFIG_FILE = System.getProperty("user.dir") |
| + File.separator + CONFIG_FILE; |
| |
| // Test properties file in [project test source directory] of CI. |
| String TEST_CONFIG_FILE = System.getProperty("user.dir") |
| + File.separator + "tst-rsrc" + File.separator + CONFIG_FILE; |
| |
| } |
| |
| /** |
| * Find test properties from various sources in order of priority. |
| */ |
| static class Props implements WalkEncryptionTest.Names, AmazonS3.Keys { |
| |
| static boolean haveEnvVar(String name) { |
| return System.getenv(name) != null; |
| } |
| |
| static boolean haveEnvVarFile(String name) { |
| return haveEnvVar(name) && new File(name).exists(); |
| } |
| |
| static boolean haveSysProp(String name) { |
| return System.getProperty(name) != null; |
| } |
| |
| static boolean haveSysPropFile(String name) { |
| return haveSysProp(name) && new File(name).exists(); |
| } |
| |
| static void loadEnvVar(String source, String target, Properties props) { |
| props.put(target, System.getenv(source)); |
| } |
| |
| static void loadSysProp(String source, String target, |
| Properties props) { |
| props.put(target, System.getProperty(source)); |
| } |
| |
| static boolean haveProp(String name, Properties props) { |
| return props.containsKey(name); |
| } |
| |
| static boolean checkTestProps(Properties props) { |
| return haveProp(ACCESS_KEY, props) && haveProp(SECRET_KEY, props) |
| && haveProp(TEST_BUCKET, props); |
| } |
| |
| static Properties fromEnvVars() { |
| if (haveEnvVar(ENV_ACCESS_KEY) && haveEnvVar(ENV_SECRET_KEY) |
| && haveEnvVar(ENV_BUCKET_NAME)) { |
| Properties props = new Properties(); |
| loadEnvVar(ENV_ACCESS_KEY, ACCESS_KEY, props); |
| loadEnvVar(ENV_SECRET_KEY, SECRET_KEY, props); |
| loadEnvVar(ENV_BUCKET_NAME, TEST_BUCKET, props); |
| return props; |
| } |
| return null; |
| } |
| |
| static Properties fromEnvFile() throws Exception { |
| if (haveEnvVarFile(ENV_CONFIG_FILE)) { |
| Properties props = new Properties(); |
| props.load(new FileInputStream(ENV_CONFIG_FILE)); |
| if (checkTestProps(props)) { |
| return props; |
| } |
| throw new Error("Environment config file is incomplete."); |
| } |
| return null; |
| } |
| |
| static Properties fromSysProps() { |
| if (haveSysProp(SYS_ACCESS_KEY) && haveSysProp(SYS_SECRET_KEY) |
| && haveSysProp(SYS_BUCKET_NAME)) { |
| Properties props = new Properties(); |
| loadSysProp(SYS_ACCESS_KEY, ACCESS_KEY, props); |
| loadSysProp(SYS_SECRET_KEY, SECRET_KEY, props); |
| loadSysProp(SYS_BUCKET_NAME, TEST_BUCKET, props); |
| return props; |
| } |
| return null; |
| } |
| |
| static Properties fromSysFile() throws Exception { |
| if (haveSysPropFile(SYS_CONFIG_FILE)) { |
| Properties props = new Properties(); |
| props.load(new FileInputStream(SYS_CONFIG_FILE)); |
| if (checkTestProps(props)) { |
| return props; |
| } |
| throw new Error("System props config file is incomplete."); |
| } |
| return null; |
| } |
| |
| static Properties fromConfigFile(String path) throws Exception { |
| File file = new File(path); |
| if (file.exists()) { |
| Properties props = new Properties(); |
| props.load(new FileInputStream(file)); |
| if (checkTestProps(props)) { |
| return props; |
| } |
| throw new Error("Props config file is incomplete: " + path); |
| } |
| return null; |
| } |
| |
| /** |
| * Find test properties from various sources in order of priority. |
| * |
| * @return result |
| * @throws Exception |
| */ |
| static Properties discover() throws Exception { |
| Properties props; |
| if ((props = fromEnvVars()) != null) { |
| logger.debug( |
| "Using test properties from environment variables."); |
| return props; |
| } |
| if ((props = fromEnvFile()) != null) { |
| logger.debug( |
| "Using test properties from environment variable config file."); |
| return props; |
| } |
| if ((props = fromSysProps()) != null) { |
| logger.debug("Using test properties from system properties."); |
| return props; |
| } |
| if ((props = fromSysFile()) != null) { |
| logger.debug( |
| "Using test properties from system property config file."); |
| return props; |
| } |
| if ((props = fromConfigFile(HOME_CONFIG_FILE)) != null) { |
| logger.debug( |
| "Using test properties from hard coded ${user.home} file."); |
| return props; |
| } |
| if ((props = fromConfigFile(WORK_CONFIG_FILE)) != null) { |
| logger.debug( |
| "Using test properties from hard coded ${user.dir} file."); |
| return props; |
| } |
| if ((props = fromConfigFile(TEST_CONFIG_FILE)) != null) { |
| logger.debug( |
| "Using test properties from hard coded ${project.source} file."); |
| return props; |
| } |
| throw new Error("Can not load test properties form any source."); |
| } |
| |
| } |
| |
| /** |
| * Collection of test utility methods. |
| */ |
| static class Util { |
| |
| /** |
| * Read UTF-8 encoded text file into string. |
| * |
| * @param file |
| * @return result |
| * @throws Exception |
| */ |
| static String textRead(File file) throws Exception { |
| return new String(Files.readAllBytes(file.toPath()), UTF_8); |
| } |
| |
| /** |
| * Write string into UTF-8 encoded file. |
| * |
| * @param file |
| * @param text |
| * @throws Exception |
| */ |
| static void textWrite(File file, String text) throws Exception { |
| Files.write(file.toPath(), text.getBytes(UTF_8)); |
| } |
| |
| static void verifyFileContent(File fileOne, File fileTwo) |
| throws Exception { |
| assertTrue(fileOne.length() > 0); |
| assertTrue(fileTwo.length() > 0); |
| String textOne = textRead(fileOne); |
| String textTwo = textRead(fileTwo); |
| assertEquals(textOne, textTwo); |
| } |
| |
| /** |
| * Create local folder. |
| * |
| * @param folder |
| * @throws Exception |
| */ |
| static void folderCreate(String folder) throws Exception { |
| File path = new File(folder); |
| assertTrue(path.mkdirs()); |
| } |
| |
| /** |
| * Delete local folder. |
| * |
| * @param folder |
| * @throws Exception |
| */ |
| static void folderDelete(String folder) throws Exception { |
| File path = new File(folder); |
| FileUtils.delete(path, |
| FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); |
| } |
| |
| /** |
| * Discover public address of CI server. |
| * |
| * @return result |
| * @throws Exception |
| */ |
| static String publicAddress() throws Exception { |
| try { |
| String service = "http://checkip.amazonaws.com"; |
| URL url = new URL(service); |
| URLConnection c = url.openConnection(); |
| c.setConnectTimeout(500); |
| c.setReadTimeout(500); |
| try (BufferedReader reader = new BufferedReader( |
| new InputStreamReader(c.getInputStream(), UTF_8))) { |
| return reader.readLine(); |
| } |
| } catch (UnknownHostException | SocketTimeoutException e) { |
| return "Can't reach http://checkip.amazonaws.com to" |
| + " determine public address"; |
| } |
| } |
| |
| /** |
| * Discover Password-Based Encryption (PBE) engines providing both |
| * [SecretKeyFactory] and [AlgorithmParameters]. |
| * |
| * @return result |
| */ |
| // https://www.bouncycastle.org/specifications.html |
| // https://docs.oracle.com/javase/8/docs/technotes/guides/security/SunProviders.html |
| static List<String> cryptoCipherListPBE() { |
| return cryptoCipherList(WalkEncryption.Vals.REGEX_PBE); |
| } |
| |
| // TODO returns inconsistent list. |
| static List<String> cryptoCipherListTrans() { |
| return cryptoCipherList(WalkEncryption.Vals.REGEX_TRANS); |
| } |
| |
| static String securityProviderName(String algorithm) throws Exception { |
| return SecretKeyFactory.getInstance(algorithm).getProvider() |
| .getName(); |
| } |
| |
| static List<String> cryptoCipherList(String regex) { |
| Set<String> source = Security.getAlgorithms("Cipher"); |
| Set<String> target = new TreeSet<>(); |
| for (String algo : source) { |
| algo = algo.toUpperCase(Locale.ROOT); |
| if (algo.matches(regex)) { |
| target.add(algo); |
| } |
| } |
| return new ArrayList<>(target); |
| } |
| |
| /** |
| * Stream copy. |
| * |
| * @param from |
| * @param into |
| * @return count |
| * @throws IOException |
| */ |
| static long transferStream(InputStream from, OutputStream into) |
| throws IOException { |
| byte[] array = new byte[1 * 1024]; |
| long total = 0; |
| while (true) { |
| int count = from.read(array); |
| if (count == -1) { |
| break; |
| } |
| into.write(array, 0, count); |
| total += count; |
| } |
| return total; |
| } |
| |
| /** |
| * Setup proxy during CI build. |
| * |
| * @throws Exception |
| */ |
| // https://wiki.eclipse.org/Hudson#Accessing_the_Internet_using_Proxy |
| // http://docs.oracle.com/javase/7/docs/api/java/net/doc-files/net-properties.html |
| static void proxySetup() throws Exception { |
| String keyNoProxy = "no_proxy"; |
| String keyHttpProxy = "http_proxy"; |
| String keyHttpsProxy = "https_proxy"; |
| |
| String no_proxy = System.getProperty(keyNoProxy, |
| System.getenv(keyNoProxy)); |
| if (no_proxy != null) { |
| System.setProperty("http.nonProxyHosts", no_proxy); |
| logger.info("Proxy NOT: " + no_proxy); |
| } |
| |
| String http_proxy = System.getProperty(keyHttpProxy, |
| System.getenv(keyHttpProxy)); |
| if (http_proxy != null) { |
| URL url = new URL(http_proxy); |
| System.setProperty("http.proxyHost", url.getHost()); |
| System.setProperty("http.proxyPort", "" + url.getPort()); |
| logger.info("Proxy HTTP: " + http_proxy); |
| } |
| |
| String https_proxy = System.getProperty(keyHttpsProxy, |
| System.getenv(keyHttpsProxy)); |
| if (https_proxy != null) { |
| URL url = new URL(https_proxy); |
| System.setProperty("https.proxyHost", url.getHost()); |
| System.setProperty("https.proxyPort", "" + url.getPort()); |
| logger.info("Proxy HTTPS: " + https_proxy); |
| } |
| |
| if (no_proxy == null && http_proxy == null && https_proxy == null) { |
| logger.info("Proxy not used."); |
| } |
| |
| } |
| |
| /** |
| * Permit long tests on CI or with manual activation. |
| * |
| * @return result |
| */ |
| static boolean permitLongTests() { |
| return isBuildCI() || isProfileActive(); |
| } |
| |
| /** |
| * Using Maven profile activation, see pom.xml |
| * |
| * @return result |
| */ |
| static boolean isProfileActive() { |
| return Boolean.parseBoolean(System.getProperty("jgit.test.long")); |
| } |
| |
| /** |
| * Detect if build is running on CI. |
| * |
| * @return result |
| */ |
| static boolean isBuildCI() { |
| return System.getenv("HUDSON_HOME") != null; |
| } |
| |
| /** |
| * Setup JCE security policy restrictions. Can remove restrictions when |
| * restrictions are present, but can not impose them when restrictions |
| * are missing. |
| * |
| * @param restrictedOn |
| */ |
| // http://www.docjar.com/html/api/javax/crypto/JceSecurity.java.html |
| static void policySetup(boolean restrictedOn) { |
| try { |
| java.lang.reflect.Field isRestricted = Class |
| .forName("javax.crypto.JceSecurity") |
| .getDeclaredField("isRestricted"); |
| isRestricted.setAccessible(true); |
| isRestricted.set(null, Boolean.valueOf(restrictedOn)); |
| } catch (Throwable e) { |
| logger.info( |
| "Could not setup JCE security policy restrictions."); |
| } |
| } |
| |
| static void reportPolicy() { |
| try { |
| java.lang.reflect.Field isRestricted = Class |
| .forName("javax.crypto.JceSecurity") |
| .getDeclaredField("isRestricted"); |
| isRestricted.setAccessible(true); |
| logger.info("JCE security policy restricted=" |
| + isRestricted.get(null)); |
| } catch (Throwable e) { |
| logger.info( |
| "Could not report JCE security policy restrictions."); |
| } |
| } |
| |
| static List<Object[]> product(List<String> one, List<String> two) { |
| List<Object[]> result = new ArrayList<>(); |
| for (String s1 : one) { |
| for (String s2 : two) { |
| result.add(new Object[] { s1, s2 }); |
| } |
| } |
| return result; |
| } |
| |
| } |
| |
| /** |
| * Common base for encryption tests. |
| */ |
| @FixMethodOrder(MethodSorters.NAME_ASCENDING) |
| public abstract static class Base extends SampleDataRepositoryTestCase { |
| |
| /** |
| * S3 URI user used by JGIT to discover connection configuration file. |
| */ |
| static final String JGIT_USER = "tester-" + System.currentTimeMillis(); |
| |
| /** |
| * S3 content encoding password used for this test session. |
| */ |
| static final String JGIT_PASS = "secret-" + System.currentTimeMillis(); |
| |
| /** |
| * S3 repository configuration file expected by {@link AmazonS3}. |
| */ |
| static final String JGIT_CONF_FILE = System.getProperty("user.home") |
| + "/" + JGIT_USER; |
| |
| /** |
| * Name representing remote or local JGIT repository. |
| */ |
| static final String JGIT_REPO_DIR = JGIT_USER + ".jgit"; |
| |
| /** |
| * Local JGIT repository for this test session. |
| */ |
| static final String JGIT_LOCAL_DIR = System.getProperty("user.dir") |
| + "/target/" + JGIT_REPO_DIR; |
| |
| /** |
| * Remote JGIT repository for this test session. |
| */ |
| static final String JGIT_REMOTE_DIR = JGIT_REPO_DIR; |
| |
| /** |
| * Generate JGIT S3 connection configuration file. |
| * |
| * @param algorithm |
| * @throws Exception |
| */ |
| static void configCreate(String algorithm) throws Exception { |
| Properties props = Props.discover(); |
| props.put(AmazonS3.Keys.PASSWORD, JGIT_PASS); |
| props.put(AmazonS3.Keys.CRYPTO_ALG, algorithm); |
| try (PrintWriter writer = new PrintWriter(JGIT_CONF_FILE, |
| UTF_8.name())) { |
| props.store(writer, "JGIT S3 connection configuration file."); |
| } |
| } |
| |
| /** |
| * Generate JGIT S3 connection configuration file. |
| * |
| * @param source |
| * @throws Exception |
| */ |
| static void configCreate(Properties source) throws Exception { |
| Properties target = Props.discover(); |
| target.putAll(source); |
| try (PrintWriter writer = new PrintWriter(JGIT_CONF_FILE, |
| UTF_8.name())) { |
| target.store(writer, "JGIT S3 connection configuration file."); |
| } |
| } |
| |
| /** |
| * Remove JGIT connection configuration file. |
| * |
| * @throws Exception |
| */ |
| static void configDelete() throws Exception { |
| File path = new File(JGIT_CONF_FILE); |
| FileUtils.delete(path, FileUtils.SKIP_MISSING); |
| } |
| |
| /** |
| * Generate remote URI for the test session. |
| * |
| * @return result |
| * @throws Exception |
| */ |
| static String amazonURI() throws Exception { |
| Properties props = Props.discover(); |
| String bucket = props.getProperty(Names.TEST_BUCKET); |
| assertNotNull(bucket); |
| return TransportAmazonS3.S3_SCHEME + "://" + JGIT_USER + "@" |
| + bucket + "/" + JGIT_REPO_DIR; |
| } |
| |
| /** |
| * Create S3 repository folder. |
| * |
| * @throws Exception |
| */ |
| static void remoteCreate() throws Exception { |
| Properties props = Props.discover(); |
| props.remove(AmazonS3.Keys.PASSWORD); // Disable encryption. |
| String bucket = props.getProperty(Names.TEST_BUCKET); |
| AmazonS3 s3 = new AmazonS3(props); |
| String path = JGIT_REMOTE_DIR + "/"; |
| s3.put(bucket, path, new byte[0]); |
| logger.debug("remote create: " + JGIT_REMOTE_DIR); |
| } |
| |
| /** |
| * Delete S3 repository folder. |
| * |
| * @throws Exception |
| */ |
| static void remoteDelete() throws Exception { |
| Properties props = Props.discover(); |
| props.remove(AmazonS3.Keys.PASSWORD); // Disable encryption. |
| String bucket = props.getProperty(Names.TEST_BUCKET); |
| AmazonS3 s3 = new AmazonS3(props); |
| List<String> list = s3.list(bucket, JGIT_REMOTE_DIR); |
| for (String path : list) { |
| path = JGIT_REMOTE_DIR + "/" + path; |
| s3.delete(bucket, path); |
| } |
| logger.debug("remote delete: " + JGIT_REMOTE_DIR); |
| } |
| |
| /** |
| * Verify if we can create/delete remote file. |
| * |
| * @throws Exception |
| */ |
| static void remoteVerify() throws Exception { |
| Properties props = Props.discover(); |
| String bucket = props.getProperty(Names.TEST_BUCKET); |
| AmazonS3 s3 = new AmazonS3(props); |
| String file = JGIT_USER + "-" + UUID.randomUUID().toString(); |
| String path = JGIT_REMOTE_DIR + "/" + file; |
| s3.put(bucket, path, file.getBytes(UTF_8)); |
| s3.delete(bucket, path); |
| } |
| |
| /** |
| * Verify if any security provider published the algorithm. |
| * |
| * @param algorithm |
| * @return result |
| */ |
| static boolean isAlgorithmPresent(String algorithm) { |
| Set<String> cipherSet = Security.getAlgorithms("Cipher"); |
| for (String source : cipherSet) { |
| // Standard names are not case-sensitive. |
| // http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html |
| String target = algorithm.toUpperCase(Locale.ROOT); |
| if (source.equalsIgnoreCase(target)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| static boolean isAlgorithmPresent(Properties props) { |
| String profile = props.getProperty(AmazonS3.Keys.CRYPTO_ALG); |
| String version = props.getProperty(AmazonS3.Keys.CRYPTO_VER, |
| WalkEncryption.Vals.DEFAULT_VERS); |
| String cryptoAlgo; |
| String keyAlgo; |
| switch (version) { |
| case WalkEncryption.Vals.DEFAULT_VERS: |
| case WalkEncryption.JGitV1.VERSION: |
| cryptoAlgo = profile; |
| keyAlgo = profile; |
| break; |
| case WalkEncryption.JGitV2.VERSION: |
| cryptoAlgo = props |
| .getProperty(profile + WalkEncryption.Keys.X_ALGO); |
| keyAlgo = props |
| .getProperty(profile + WalkEncryption.Keys.X_KEY_ALGO); |
| break; |
| default: |
| return false; |
| } |
| try { |
| InsecureCipherFactory.create(cryptoAlgo); |
| SecretKeyFactory.getInstance(keyAlgo); |
| return true; |
| } catch (Throwable e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Verify if JRE security policy allows the algorithm. |
| * |
| * @param algorithm |
| * @return result |
| */ |
| static boolean isAlgorithmAllowed(String algorithm) { |
| try { |
| WalkEncryption crypto = new WalkEncryption.JetS3tV2( |
| algorithm, JGIT_PASS); |
| verifyCrypto(crypto); |
| return true; |
| } catch (IOException e) { |
| return false; // Encryption failure. |
| } catch (GeneralSecurityException e) { |
| throw new Error(e); // Construction failure. |
| } |
| } |
| |
| static boolean isAlgorithmAllowed(Properties props) { |
| try { |
| WalkEncryption.instance(props); |
| return true; |
| } catch (GeneralSecurityException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Verify round trip encryption. |
| * |
| * @param crypto |
| * @throws IOException |
| */ |
| static void verifyCrypto(WalkEncryption crypto) throws IOException { |
| String charset = "UTF-8"; |
| String sourceText = "secret-message Свобода 老子"; |
| String targetText; |
| byte[] cipherText; |
| { |
| byte[] origin = sourceText.getBytes(charset); |
| ByteArrayOutputStream target = new ByteArrayOutputStream(); |
| try (OutputStream source = crypto.encrypt(target)) { |
| source.write(origin); |
| source.flush(); |
| } |
| cipherText = target.toByteArray(); |
| } |
| { |
| InputStream source = new ByteArrayInputStream(cipherText); |
| InputStream target = crypto.decrypt(source); |
| ByteArrayOutputStream result = new ByteArrayOutputStream(); |
| transferStream(target, result); |
| targetText = result.toString(charset); |
| } |
| assertEquals(sourceText, targetText); |
| } |
| |
| /** |
| * Algorithm is testable when it is present and allowed by policy. |
| * |
| * @param algorithm |
| * @return result |
| */ |
| static boolean isAlgorithmTestable(String algorithm) { |
| return isAlgorithmPresent(algorithm) |
| && isAlgorithmAllowed(algorithm); |
| } |
| |
| static boolean isAlgorithmTestable(Properties props) { |
| return isAlgorithmPresent(props) && isAlgorithmAllowed(props); |
| } |
| |
| /** |
| * Log algorithm, provider, testability. |
| * |
| * @param algorithm |
| * @throws Exception |
| */ |
| static void reportAlgorithmStatus(String algorithm) throws Exception { |
| final boolean present = isAlgorithmPresent(algorithm); |
| final boolean allowed = present && isAlgorithmAllowed(algorithm); |
| final String provider = present ? securityProviderName(algorithm) |
| : "N/A"; |
| String status = "Algorithm: " + algorithm + " @ " + provider + "; " |
| + "present/allowed : " + present + "/" + allowed; |
| if (allowed) { |
| logger.info("Testing " + status); |
| } else { |
| logger.warn("Missing " + status); |
| } |
| } |
| |
| static void reportAlgorithmStatus(Properties props) throws Exception { |
| final boolean present = isAlgorithmPresent(props); |
| final boolean allowed = present && isAlgorithmAllowed(props); |
| |
| String profile = props.getProperty(AmazonS3.Keys.CRYPTO_ALG); |
| String version = props.getProperty(AmazonS3.Keys.CRYPTO_VER); |
| |
| StringBuilder status = new StringBuilder(); |
| status.append(" Version: " + version); |
| status.append(" Profile: " + profile); |
| status.append(" Present: " + present); |
| status.append(" Allowed: " + allowed); |
| |
| if (allowed) { |
| logger.info("Testing " + status); |
| } else { |
| logger.warn("Missing " + status); |
| } |
| } |
| |
| /** |
| * Verify if we can perform remote tests. |
| * |
| * @return result |
| */ |
| static boolean isTestConfigPresent() { |
| try { |
| Props.discover(); |
| return true; |
| } catch (Throwable e) { |
| return false; |
| } |
| } |
| |
| static void reportTestConfigPresent() { |
| if (isTestConfigPresent()) { |
| logger.info("Amazon S3 test configuration is present."); |
| } else { |
| logger.error( |
| "Amazon S3 test configuration is missing, tests will not run."); |
| } |
| } |
| |
| /** |
| * Log public address of CI. |
| * |
| * @throws Exception |
| */ |
| static void reportPublicAddress() throws Exception { |
| logger.info("Public address: " + publicAddress()); |
| } |
| |
| /** |
| * BouncyCastle provider class. |
| * |
| * Needs extra dependency, see pom.xml |
| */ |
| // http://search.maven.org/#artifactdetails%7Corg.bouncycastle%7Cbcprov-jdk15on%7C1.52%7Cjar |
| static final String PROVIDER_BC = "org.bouncycastle.jce.provider.BouncyCastleProvider"; |
| |
| /** |
| * Load BouncyCastle provider if present. |
| */ |
| static void loadBouncyCastle() { |
| try { |
| Class<?> provider = Class.forName(PROVIDER_BC); |
| Provider instance = (Provider) provider |
| .getConstructor(new Class[] {}) |
| .newInstance(new Object[] {}); |
| Security.addProvider(instance); |
| logger.info("Loaded " + PROVIDER_BC); |
| } catch (Throwable e) { |
| logger.warn("Failed to load " + PROVIDER_BC); |
| } |
| } |
| |
| static void reportLongTests() { |
| if (permitLongTests()) { |
| logger.info("Long running tests are enabled."); |
| } else { |
| logger.warn("Long running tests are disabled."); |
| } |
| } |
| |
| /** |
| * Non-PBE algorithm, for error check. |
| */ |
| static final String ALGO_ERROR = "PBKDF2WithHmacSHA1"; |
| |
| /** |
| * Default JetS3t algorithm present in most JRE. |
| */ |
| static final String ALGO_JETS3T = "PBEWithMD5AndDES"; |
| |
| /** |
| * Minimal strength AES based algorithm present in most JRE. |
| */ |
| static final String ALGO_MINIMAL_AES = "PBEWithHmacSHA1AndAES_128"; |
| |
| /** |
| * Selected non-AES algorithm present in BouncyCastle provider. |
| */ |
| static final String ALGO_BOUNCY_CASTLE_CBC = "PBEWithSHAAndTwofish-CBC"; |
| |
| ////////////////////////////////////////////////// |
| |
| @BeforeClass |
| public static void initialize() throws Exception { |
| Transport.register(TransportAmazonS3.PROTO_S3); |
| proxySetup(); |
| reportPolicy(); |
| reportLongTests(); |
| reportPublicAddress(); |
| reportTestConfigPresent(); |
| loadBouncyCastle(); |
| if (isTestConfigPresent()) { |
| remoteCreate(); |
| } |
| } |
| |
| @AfterClass |
| public static void terminate() throws Exception { |
| configDelete(); |
| folderDelete(JGIT_LOCAL_DIR); |
| if (isTestConfigPresent()) { |
| remoteDelete(); |
| } |
| } |
| |
| @Before |
| @Override |
| public void setUp() throws Exception { |
| super.setUp(); |
| } |
| |
| @After |
| @Override |
| public void tearDown() throws Exception { |
| super.tearDown(); |
| } |
| |
| /** |
| * Optional encrypted amazon remote JGIT life cycle test. |
| * |
| * @param props |
| * @throws Exception |
| */ |
| void cryptoTestIfCan(Properties props) throws Exception { |
| reportAlgorithmStatus(props); |
| assumeTrue(isTestConfigPresent()); |
| assumeTrue(isAlgorithmTestable(props)); |
| cryptoTest(props); |
| } |
| |
| /** |
| * Required encrypted amazon remote JGIT life cycle test. |
| * |
| * @param props |
| * @throws Exception |
| */ |
| void cryptoTest(Properties props) throws Exception { |
| |
| remoteDelete(); |
| configCreate(props); |
| folderDelete(JGIT_LOCAL_DIR); |
| |
| String uri = amazonURI(); |
| |
| // Local repositories. |
| File dirOne = db.getWorkTree(); // Provided by setup. |
| File dirTwo = new File(JGIT_LOCAL_DIR); |
| |
| // Local verification files. |
| String nameStatic = "master.txt"; // Provided by setup. |
| String nameDynamic = JGIT_USER + "-" + UUID.randomUUID().toString(); |
| |
| String remote = "remote"; |
| RefSpec specs = new RefSpec("refs/heads/master:refs/heads/master"); |
| |
| { // Push into remote from local one. |
| |
| StoredConfig config = db.getConfig(); |
| RemoteConfig remoteConfig = new RemoteConfig(config, remote); |
| remoteConfig.addURI(new URIish(uri)); |
| remoteConfig.update(config); |
| config.save(); |
| |
| try (Git git = Git.open(dirOne)) { |
| git.checkout().setName("master").call(); |
| git.push().setRemote(remote).setRefSpecs(specs).call(); |
| } |
| |
| File fileStatic = new File(dirOne, nameStatic); |
| assertTrue("Provided by setup", fileStatic.exists()); |
| |
| } |
| |
| { // Clone from remote into local two. |
| |
| File fileStatic = new File(dirTwo, nameStatic); |
| assertFalse("Not Provided by setup", fileStatic.exists()); |
| |
| try (Git git = Git.cloneRepository().setURI(uri) |
| .setDirectory(dirTwo).call()) { |
| assertTrue("Provided by clone", fileStatic.exists()); |
| } |
| |
| } |
| |
| { // Verify static file content. |
| File fileOne = new File(dirOne, nameStatic); |
| File fileTwo = new File(dirTwo, nameStatic); |
| verifyFileContent(fileOne, fileTwo); |
| } |
| |
| { // Verify new file commit and push from local one. |
| |
| File fileDynamic = new File(dirOne, nameDynamic); |
| assertFalse("Not Provided by setup", fileDynamic.exists()); |
| FileUtils.createNewFile(fileDynamic); |
| textWrite(fileDynamic, nameDynamic); |
| assertTrue("Provided by create", fileDynamic.exists()); |
| assertTrue("Need content to encrypt", fileDynamic.length() > 0); |
| |
| try (Git git = Git.open(dirOne)) { |
| git.add().addFilepattern(nameDynamic).call(); |
| git.commit().setMessage(nameDynamic).call(); |
| git.push().setRemote(remote).setRefSpecs(specs).call(); |
| } |
| |
| } |
| |
| { // Verify new file pull from remote into local two. |
| |
| File fileDynamic = new File(dirTwo, nameDynamic); |
| assertFalse("Not Provided by setup", fileDynamic.exists()); |
| |
| try (Git git = Git.open(dirTwo)) { |
| git.pull().call(); |
| } |
| |
| assertTrue("Provided by pull", fileDynamic.exists()); |
| } |
| |
| { // Verify dynamic file content. |
| File fileOne = new File(dirOne, nameDynamic); |
| File fileTwo = new File(dirTwo, nameDynamic); |
| verifyFileContent(fileOne, fileTwo); |
| } |
| |
| } |
| |
| } |
| |
| /** |
| * Verify prerequisites. |
| */ |
| @FixMethodOrder(MethodSorters.NAME_ASCENDING) |
| public static class Required extends Base { |
| |
| @Test |
| public void test_A1_ValidURI() throws Exception { |
| assumeTrue(isTestConfigPresent()); |
| URIish uri = new URIish(amazonURI()); |
| assertTrue("uri=" + uri, TransportAmazonS3.PROTO_S3.canHandle(uri)); |
| } |
| |
| @Test(expected = Exception.class) |
| public void test_A2_CryptoError() throws Exception { |
| assumeTrue(isTestConfigPresent()); |
| Properties props = new Properties(); |
| props.put(AmazonS3.Keys.CRYPTO_ALG, ALGO_ERROR); |
| props.put(AmazonS3.Keys.PASSWORD, JGIT_PASS); |
| cryptoTest(props); |
| } |
| |
| } |
| |
| /** |
| * Test minimal set of algorithms. |
| */ |
| @FixMethodOrder(MethodSorters.NAME_ASCENDING) |
| public static class MinimalSet extends Base { |
| |
| @Test |
| public void test_V0_Java7_JET() throws Exception { |
| assumeTrue(isTestConfigPresent()); |
| Properties props = new Properties(); |
| props.put(AmazonS3.Keys.CRYPTO_ALG, ALGO_JETS3T); |
| // Do not set version. |
| props.put(AmazonS3.Keys.PASSWORD, JGIT_PASS); |
| cryptoTestIfCan(props); |
| } |
| |
| @Test |
| public void test_V1_Java7_GIT() throws Exception { |
| assumeTrue(isTestConfigPresent()); |
| Properties props = new Properties(); |
| props.put(AmazonS3.Keys.CRYPTO_ALG, ALGO_JETS3T); |
| props.put(AmazonS3.Keys.CRYPTO_VER, "1"); |
| props.put(AmazonS3.Keys.PASSWORD, JGIT_PASS); |
| cryptoTestIfCan(props); |
| } |
| |
| @Test |
| public void test_V2_Java7_AES() throws Exception { |
| assumeTrue(isTestConfigPresent()); |
| // String profile = "default"; |
| String profile = "AES/CBC/PKCS5Padding+PBKDF2WithHmacSHA1"; |
| Properties props = new Properties(); |
| props.put(AmazonS3.Keys.CRYPTO_ALG, profile); |
| props.put(AmazonS3.Keys.CRYPTO_VER, "2"); |
| props.put(AmazonS3.Keys.PASSWORD, JGIT_PASS); |
| props.put(profile + WalkEncryption.Keys.X_ALGO, "AES/CBC/PKCS5Padding"); |
| props.put(profile + WalkEncryption.Keys.X_KEY_ALGO, "PBKDF2WithHmacSHA1"); |
| props.put(profile + WalkEncryption.Keys.X_KEY_SIZE, "128"); |
| props.put(profile + WalkEncryption.Keys.X_KEY_ITER, "10000"); |
| props.put(profile + WalkEncryption.Keys.X_KEY_SALT, "e2 55 89 67 8e 8d e8 4c"); |
| cryptoTestIfCan(props); |
| } |
| |
| @Test |
| public void test_V2_Java8_PBE_AES() throws Exception { |
| assumeTrue(isTestConfigPresent()); |
| String profile = "PBEWithHmacSHA512AndAES_256"; |
| Properties props = new Properties(); |
| props.put(AmazonS3.Keys.CRYPTO_ALG, profile); |
| props.put(AmazonS3.Keys.CRYPTO_VER, "2"); |
| props.put(AmazonS3.Keys.PASSWORD, JGIT_PASS); |
| props.put(profile + WalkEncryption.Keys.X_ALGO, "PBEWithHmacSHA512AndAES_256"); |
| props.put(profile + WalkEncryption.Keys.X_KEY_ALGO, "PBEWithHmacSHA512AndAES_256"); |
| props.put(profile + WalkEncryption.Keys.X_KEY_SIZE, "256"); |
| props.put(profile + WalkEncryption.Keys.X_KEY_ITER, "10000"); |
| props.put(profile + WalkEncryption.Keys.X_KEY_SALT, "e2 55 89 67 8e 8d e8 4c"); |
| policySetup(false); |
| cryptoTestIfCan(props); |
| } |
| |
| } |
| |
| /** |
| * Test all present and allowed PBE algorithms. |
| */ |
| // https://github.com/junit-team/junit/wiki/Parameterized-tests |
| @RunWith(Parameterized.class) |
| @FixMethodOrder(MethodSorters.NAME_ASCENDING) |
| public static class TestablePBE extends Base { |
| |
| @Parameters(name = "Profile: {0} Version: {1}") |
| public static Collection<Object[]> argsList() { |
| List<String> algorithmList = new ArrayList<>(); |
| algorithmList.addAll(cryptoCipherListPBE()); |
| |
| List<String> versionList = new ArrayList<>(); |
| versionList.add("0"); |
| versionList.add("1"); |
| |
| return product(algorithmList, versionList); |
| } |
| |
| final String profile; |
| |
| final String version; |
| |
| final String password = JGIT_PASS; |
| |
| public TestablePBE(String profile, String version) { |
| this.profile = profile; |
| this.version = version; |
| } |
| |
| @Test |
| public void testCrypto() throws Exception { |
| assumeTrue(permitLongTests()); |
| Properties props = new Properties(); |
| props.put(AmazonS3.Keys.CRYPTO_ALG, profile); |
| props.put(AmazonS3.Keys.CRYPTO_VER, version); |
| props.put(AmazonS3.Keys.PASSWORD, password); |
| cryptoTestIfCan(props); |
| } |
| |
| } |
| |
| /** |
| * Test all present and allowed transformation algorithms. |
| */ |
| // https://github.com/junit-team/junit/wiki/Parameterized-tests |
| @RunWith(Parameterized.class) |
| @FixMethodOrder(MethodSorters.NAME_ASCENDING) |
| public static class TestableTransformation extends Base { |
| |
| @Parameters(name = "Profile: {0} Version: {1}") |
| public static Collection<Object[]> argsList() { |
| List<String> algorithmList = new ArrayList<>(); |
| algorithmList.addAll(cryptoCipherListTrans()); |
| |
| List<String> versionList = new ArrayList<>(); |
| versionList.add("1"); |
| |
| return product(algorithmList, versionList); |
| } |
| |
| final String profile; |
| |
| final String version; |
| |
| final String password = JGIT_PASS; |
| |
| public TestableTransformation(String profile, String version) { |
| this.profile = profile; |
| this.version = version; |
| } |
| |
| @Test |
| public void testCrypto() throws Exception { |
| assumeTrue(permitLongTests()); |
| Properties props = new Properties(); |
| props.put(AmazonS3.Keys.CRYPTO_ALG, profile); |
| props.put(AmazonS3.Keys.CRYPTO_VER, version); |
| props.put(AmazonS3.Keys.PASSWORD, password); |
| cryptoTestIfCan(props); |
| } |
| |
| } |
| |
| } |