/* * Copyright (C) 2015, Andrei Pozolotin. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available * under the terms of the Eclipse Distribution License v1.0 which * accompanies this distribution, is reproduced below, and is * available at http://www.eclipse.org/org/documents/edl-v10.php * * All rights reserved. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * - Neither the name of the Eclipse Foundation, Inc. nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ 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; } else { 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; } else { throw new Error("Environment config file is incomplete."); } } else { 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; } else { 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; } else { throw new Error("System props config file is incomplete."); } } else { 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; } else { throw new Error("Props config file is incomplete: " + path); } } else { 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 cryptoCipherListPBE() { return cryptoCipherList(WalkEncryption.Vals.REGEX_PBE); } // TODO returns inconsistent list. static List cryptoCipherListTrans() { return cryptoCipherList(WalkEncryption.Vals.REGEX_TRANS); } static String securityProviderName(String algorithm) throws Exception { return SecretKeyFactory.getInstance(algorithm).getProvider() .getName(); } static List cryptoCipherList(String regex) { Set source = Security.getAlgorithms("Cipher"); Set 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 product(List one, List two) { List 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 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 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 argsList() { List algorithmList = new ArrayList<>(); algorithmList.addAll(cryptoCipherListPBE()); List 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 argsList() { List algorithmList = new ArrayList<>(); algorithmList.addAll(cryptoCipherListTrans()); List 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); } } }