]> source.dussan.org Git - jgit.git/commitdiff
Adding JGitV1 and JGitV2 Walk Encryption 44/56744/7
authorAndrei Pozolotin <andrei.pozolotin@gmail.com>
Fri, 25 Sep 2015 20:55:32 +0000 (20:55 +0000)
committerMatthias Sohn <matthias.sohn@sap.com>
Sun, 18 Oct 2015 21:22:56 +0000 (23:22 +0200)
Building on top of https://git.eclipse.org/r/#/c/56391/

Here we preserve compatibility with JetS3t
and add 2 new native JGit encryption implementations.

For reference, see connection configuration files:
* Version 0: jgit-s3-connection-v-0.properties
* Version 1: jgit-s3-connection-v-1.properties
* Version 2: jgit-s3-connection-v-2.properties

Change-Id: I713290bcacbe92d88e5ef28ce137de73dd1abe2f
Signed-off-by: Andrei Pozolotin <andrei.pozolotin@gmail.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-0.properties [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-1.properties [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-2.properties [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/WalkEncryptionTest.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java

diff --git a/org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-0.properties b/org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-0.properties
new file mode 100644 (file)
index 0000000..2402a49
--- /dev/null
@@ -0,0 +1,11 @@
+#
+# Sample Amazon S3 connection configuration file, Version 0.
+# Version 0 (or lack of version) will produce JetS3tV2 compatible encryption.
+# JetS3tV2 supports only PBE algorithms, with partially compromised AES mode.
+#
+
+accesskey = AKIAIYWXB4ETREBRM123
+secretkey = ozCuIsqxsARoPe3FFyv3F/jiMSc3Yqay7B9UF234
+
+crypto.algorithm = PBEWithMD5AndDES
+password = secret
diff --git a/org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-1.properties b/org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-1.properties
new file mode 100644 (file)
index 0000000..d0d1611
--- /dev/null
@@ -0,0 +1,14 @@
+#
+# Sample Amazon S3 connection configuration file, Version 1.
+# Version 1 will produce JGitV1 compatible encryption.
+# It is JetS3tV2-like mode with proper AES support.
+# JGitV1 uses hard coded encryption parameters.
+# JGitV1 supports only PBE algorithms.
+#
+
+accesskey = AKIAIYWXB4ETREBRM123
+secretkey = ozCuIsqxsARoPe3FFyv3F/jiMSc3Yqay7B9UF234
+
+crypto.algorithm = PBEWithHmacSHA1AndAES_128
+crypto.version = 1
+password = secret
diff --git a/org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-2.properties b/org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-2.properties
new file mode 100644 (file)
index 0000000..731b324
--- /dev/null
@@ -0,0 +1,48 @@
+#
+# Sample Amazon S3 connection configuration file, Version 2.
+# Version 2 will produce JGitV2 compatible encryption.
+# JGitV2 introduces more flexible control over cipher and key factory parameters.
+# JGitV2 hides actual cipher/key algorithms inside the encryption profile.
+# JGitV2 does not use any hard coded encryption parameters.
+# JGitV2 supports both PBE and Non-PBE algorithms.
+
+accesskey = AKIAIYWXB4ETREBRM123
+secretkey = ozCuIsqxsARoPe3FFyv3F/jiMSc3Yqay7B9UF234
+
+# In Version 2 "crypto.algorithm" is a reference to the encryption "profile".
+crypto.algorithm = custom
+crypto.version = 2
+password = secret
+
+#
+# Encryption profile is a collection of related properties,
+# all having common property root name, or prefix:
+#
+# Cipher algorithm.
+custom.algo = AES/CBC/PKCS5Padding
+# Key factory algorithm.
+custom.key.algo = PBKDF2WithHmacSHA512
+# Key size, bits.
+custom.key.size = 256
+# Number of key generation iterations.
+custom.key.iter = 50000
+# Salt used in key generation (hex value, white space OK).
+custom.key.salt = e2 55 89 67 8e 8d e8 4c
+
+# Same file can store multiple profiles.
+# Only one profile can be active at a time.
+# Active profile is selected via "crypto.algorithm"
+
+#
+# Here is how to create V1 encryption in V2 format:
+#
+# Cipher algorithm.
+legacy.algo = PBEWithHmacSHA1AndAES_128
+# Key factory algorithm.
+legacy.key.algo = PBEWithHmacSHA1AndAES_128
+# Key size, bits.
+legacy.key.size = 32
+# Number of key generation iterations.
+legacy.key.iter = 5000
+# Salt used in key generation (hex value, white space OK).
+legacy.key.salt = A40BC834D695F313
index f2701cb41f65b716ea8adc8139f4ac83860d9e7a..042d7ba78bbd9594991367b91f85ab8e91e1c636 100644 (file)
@@ -73,6 +73,7 @@ import java.util.Set;
 import java.util.TreeSet;
 import java.util.UUID;
 
+import javax.crypto.Cipher;
 import javax.crypto.SecretKeyFactory;
 
 import org.apache.log4j.Logger;
@@ -108,8 +109,10 @@ import static org.eclipse.jgit.transport.WalkEncryptionTest.Util.*;
  */
 @RunWith(Suite.class)
 @Suite.SuiteClasses({ //
+               WalkEncryptionTest.Required.class, //
                WalkEncryptionTest.MinimalSet.class, //
                WalkEncryptionTest.TestablePBE.class, //
+               WalkEncryptionTest.TestableTransformation.class, //
 })
 public class WalkEncryptionTest {
 
@@ -417,7 +420,12 @@ public class WalkEncryptionTest {
                // https://www.bouncycastle.org/specifications.html
                // https://docs.oracle.com/javase/8/docs/technotes/guides/security/SunProviders.html
                static List<String> cryptoCipherListPBE() {
-                       return cryptoCipherList("(PBE).*(WITH).+(AND).+");
+                       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 {
@@ -437,25 +445,6 @@ public class WalkEncryptionTest {
                        return new ArrayList<String>(target);
                }
 
-               /**
-                * 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();
-                               if (source.equalsIgnoreCase(target)) {
-                                       return true;
-                               }
-                       }
-                       return false;
-               }
-
                /**
                 * Stream copy.
                 *
@@ -549,6 +538,51 @@ public class WalkEncryptionTest {
                        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, new Boolean(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<Object[]>();
+                       for (String s1 : one) {
+                               for (String s2 : two) {
+                                       result.add(new Object[] { s1, s2 });
+                               }
+                       }
+                       return result;
+               }
+
        }
 
        /**
@@ -604,6 +638,20 @@ public class WalkEncryptionTest {
                        writer.close();
                }
 
+               /**
+                * Generate JGIT S3 connection configuration file.
+                *
+                * @param source
+                * @throws Exception
+                */
+               static void configCreate(Properties source) throws Exception {
+                       Properties target = Props.discover();
+                       target.putAll(source);
+                       PrintWriter writer = new PrintWriter(JGIT_CONF_FILE);
+                       target.store(writer, "JGIT S3 connection configuration file.");
+                       writer.close();
+               }
+
                /**
                 * Remove JGIT connection configuration file.
                 *
@@ -676,6 +724,55 @@ public class WalkEncryptionTest {
                        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();
+                               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 crytoAlgo;
+                       String keyAlgo;
+                       switch (version) {
+                       case WalkEncryption.Vals.DEFAULT_VERS:
+                       case WalkEncryption.JGitV1.VERSION:
+                               crytoAlgo = profile;
+                               keyAlgo = profile;
+                               break;
+                       case WalkEncryption.JGitV2.VERSION:
+                               crytoAlgo = props
+                                               .getProperty(profile + WalkEncryption.Keys.X_ALGO);
+                               keyAlgo = props
+                                               .getProperty(profile + WalkEncryption.Keys.X_KEY_ALGO);
+                               break;
+                       default:
+                               return false;
+                       }
+                       try {
+                               Cipher.getInstance(crytoAlgo);
+                               SecretKeyFactory.getInstance(keyAlgo);
+                               return true;
+                       } catch (Throwable e) {
+                               return false;
+                       }
+               }
+
                /**
                 * Verify if JRE security policy allows the algorithm.
                 *
@@ -684,7 +781,7 @@ public class WalkEncryptionTest {
                 */
                static boolean isAlgorithmAllowed(String algorithm) {
                        try {
-                               WalkEncryption crypto = new WalkEncryption.ObjectEncryptionJetS3tV2(
+                               WalkEncryption crypto = new WalkEncryption.JetS3tV2(
                                                algorithm, JGIT_PASS);
                                verifyCrypto(crypto);
                                return true;
@@ -695,6 +792,15 @@ public class WalkEncryptionTest {
                        }
                }
 
+               static boolean isAlgorithmAllowed(Properties props) {
+                       try {
+                               WalkEncryption.instance(props);
+                               return true;
+                       } catch (GeneralSecurityException e) {
+                               return false;
+                       }
+               }
+
                /**
                 * Verify round trip encryption.
                 *
@@ -736,6 +842,10 @@ public class WalkEncryptionTest {
                                        && isAlgorithmAllowed(algorithm);
                }
 
+               static boolean isAlgorithmTestable(Properties props) {
+                       return isAlgorithmPresent(props) && isAlgorithmAllowed(props);
+               }
+
                /**
                 * Log algorithm, provider, testability.
                 *
@@ -756,6 +866,26 @@ public class WalkEncryptionTest {
                        }
                }
 
+               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.
                 *
@@ -846,6 +976,7 @@ public class WalkEncryptionTest {
                public static void initialize() throws Exception {
                        Transport.register(TransportAmazonS3.PROTO_S3);
                        proxySetup();
+                       reportPolicy();
                        reportLongTests();
                        reportPublicAddress();
                        reportTestConfigPresent();
@@ -879,26 +1010,26 @@ public class WalkEncryptionTest {
                /**
                 * Optional encrypted amazon remote JGIT life cycle test.
                 *
-                * @param algorithm
+                * @param props
                 * @throws Exception
                 */
-               void cryptoTestIfCan(String algorithm) throws Exception {
-                       reportAlgorithmStatus(algorithm);
+               void cryptoTestIfCan(Properties props) throws Exception {
+                       reportAlgorithmStatus(props);
                        assumeTrue(isTestConfigPresent());
-                       assumeTrue(isAlgorithmTestable(algorithm));
-                       cryptoTest(algorithm);
+                       assumeTrue(isAlgorithmTestable(props));
+                       cryptoTest(props);
                }
 
                /**
                 * Required encrypted amazon remote JGIT life cycle test.
                 *
-                * @param algorithm
+                * @param props
                 * @throws Exception
                 */
-               void cryptoTest(String algorithm) throws Exception {
+               void cryptoTest(Properties props) throws Exception {
 
                        remoteDelete();
-                       configCreate(algorithm);
+                       configCreate(props);
                        folderDelete(JGIT_LOCAL_DIR);
 
                        String uri = amazonURI();
@@ -990,10 +1121,10 @@ public class WalkEncryptionTest {
        }
 
        /**
-        * Test minimal set of algorithms.
+        * Verify prerequisites.
         */
        @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-       public static class MinimalSet extends Base {
+       public static class Required extends Base {
 
                @Test
                public void test_A1_ValidURI() throws Exception {
@@ -1005,22 +1136,72 @@ public class WalkEncryptionTest {
                @Test(expected = Exception.class)
                public void test_A2_CryptoError() throws Exception {
                        assumeTrue(isTestConfigPresent());
-                       cryptoTest(ALGO_ERROR);
+                       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_A3_CryptoJetS3tDefault() throws Exception {
-                       cryptoTestIfCan(ALGO_JETS3T);
+               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_A4_CryptoMinimalAES() throws Exception {
-                       cryptoTestIfCan(ALGO_MINIMAL_AES);
+               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_A5_CryptoBouncyCastleCBC() throws Exception {
-                       cryptoTestIfCan(ALGO_BOUNCY_CASTLE_CBC);
+               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);
                }
 
        }
@@ -1033,26 +1214,79 @@ public class WalkEncryptionTest {
        @FixMethodOrder(MethodSorters.NAME_ASCENDING)
        public static class TestablePBE extends Base {
 
-               @Parameters(name = "Algorithm: {0}")
-               public static Collection algorimthmList() {
-                       List<String> source = cryptoCipherListPBE();
-                       List<Object[]> target = new ArrayList<Object[]>();
-                       for (String name : source) {
-                               target.add(new Object[] { name });
-                       }
-                       return target;
+               @Parameters(name = "Profile: {0}   Version: {1}")
+               public static Collection<Object[]> argsList() {
+                       List<String> algorithmList = new ArrayList<String>();
+                       algorithmList.addAll(cryptoCipherListPBE());
+
+                       List<String> versionList = new ArrayList<String>();
+                       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<String>();
+                       algorithmList.addAll(cryptoCipherListTrans());
+
+                       List<String> versionList = new ArrayList<String>();
+                       versionList.add("1");
+
+                       return product(algorithmList, versionList);
                }
 
-               final String algorithm;
+               final String profile;
+
+               final String version;
 
-               public TestablePBE(String algorithm) {
-                       this.algorithm = algorithm;
+               final String password = JGIT_PASS;
+
+               public TestableTransformation(String profile, String version) {
+                       this.profile = profile;
+                       this.version = version;
                }
 
-               @Test // Can take long time, needs activation.
-               public void test_B1_Crypto() throws Exception {
+               @Test
+               public void testCrypto() throws Exception {
                        assumeTrue(permitLongTests());
-                       cryptoTestIfCan(algorithm);
+                       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);
                }
 
        }
index e55066a8bb07fe41eac0a3a36fa17e7ed4b4c7b7..0c8ee5d4b698a71e6d006eac956727d63fd9e572 100644 (file)
@@ -256,15 +256,7 @@ public class AmazonS3 {
                        throw new IllegalArgumentException("Invalid acl: " + pacl); //$NON-NLS-1$
 
                try {
-                       final String cPas = props.getProperty(Keys.PASSWORD);
-                       if (cPas != null) {
-                               String cAlg = props.getProperty(Keys.CRYPTO_ALG);
-                               if (cAlg == null)
-                                       cAlg = WalkEncryption.ObjectEncryptionJetS3tV2.JETS3T_ALGORITHM;
-                               encryption = new WalkEncryption.ObjectEncryptionJetS3tV2(cAlg, cPas);
-                       } else {
-                               encryption = WalkEncryption.NONE;
-                       }
+                       encryption = WalkEncryption.instance(props);
                } catch (GeneralSecurityException e) {
                        throw new IllegalArgumentException(JGitText.get().invalidEncryption, e);
                }
index e93a2af3ea2fdb8759527a1de72fddfc523d6e0c..fe03bdc867a1342e3d35941bdc40586acb46432b 100644 (file)
@@ -47,9 +47,14 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.HttpURLConnection;
+import java.security.AlgorithmParameters;
 import java.security.GeneralSecurityException;
 import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.KeySpec;
 import java.text.MessageFormat;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import javax.crypto.Cipher;
 import javax.crypto.CipherInputStream;
@@ -59,8 +64,11 @@ import javax.crypto.SecretKeyFactory;
 import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.PBEKeySpec;
 import javax.crypto.spec.PBEParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.xml.bind.DatatypeConverter;
 
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.util.Base64;
 
 abstract class WalkEncryption {
        static final WalkEncryption NONE = new NoEncryption();
@@ -69,13 +77,18 @@ abstract class WalkEncryption {
 
        static final String JETS3T_CRYPTO_ALG = "jets3t-crypto-alg"; //$NON-NLS-1$
 
-       abstract OutputStream encrypt(OutputStream os) throws IOException;
+       // Note: encrypt -> request state machine, step 1.
+       abstract OutputStream encrypt(OutputStream output) throws IOException;
 
-       abstract InputStream decrypt(InputStream in) throws IOException;
+       // Note: encrypt -> request state machine, step 2.
+       abstract void request(HttpURLConnection conn, String prefix) throws IOException;
 
-       abstract void request(HttpURLConnection u, String prefix);
+       // Note: validate -> decrypt state machine, step 1.
+       abstract void validate(HttpURLConnection conn, String prefix) throws IOException;
+
+       // Note: validate -> decrypt state machine, step 2.
+       abstract InputStream decrypt(InputStream input) throws IOException;
 
-       abstract void validate(HttpURLConnection u, String prefix) throws IOException;
 
        // TODO mixed ciphers
        // consider permitting mixed ciphers to facilitate algorithm migration
@@ -173,17 +186,17 @@ abstract class WalkEncryption {
         * <li>any AES based algorithms such as "PBE...With...And...AES" will not
         * work, since they need proper IV setup
         */
-       static class ObjectEncryptionJetS3tV2 extends WalkEncryption {
+       static class JetS3tV2 extends WalkEncryption {
 
-               static final String JETS3T_VERSION = "2"; //$NON-NLS-1$
+               static final String VERSION = "2"; //$NON-NLS-1$
 
-               static final String JETS3T_ALGORITHM = "PBEWithMD5AndDES"; //$NON-NLS-1$
+               static final String ALGORITHM = "PBEWithMD5AndDES"; //$NON-NLS-1$
 
-               static final int JETS3T_ITERATIONS = 5000;
+               static final int ITERATIONS = 5000;
 
-               static final int JETS3T_KEY_SIZE = 32;
+               static final int KEY_SIZE = 32;
 
-               static final byte[] JETS3T_SALT = { //
+               static final byte[] SALT = { //
                                (byte) 0xA4, (byte) 0x0B, (byte) 0xC8, (byte) 0x34, //
                                (byte) 0xD6, (byte) 0x95, (byte) 0xF3, (byte) 0x13 //
                };
@@ -191,7 +204,7 @@ abstract class WalkEncryption {
                // Size 16, see com.sun.crypto.provider.AESConstants.AES_BLOCK_SIZE
                static final byte[] ZERO_AES_IV = new byte[16];
 
-               private final String cryptoVer = JETS3T_VERSION;
+               private static final String cryptoVer = VERSION;
 
                private final String cryptoAlg;
 
@@ -199,10 +212,13 @@ abstract class WalkEncryption {
 
                private final AlgorithmParameterSpec paramSpec;
 
-               ObjectEncryptionJetS3tV2(final String algo, final String key)
+               JetS3tV2(final String algo, final String key)
                                throws GeneralSecurityException {
                        cryptoAlg = algo;
 
+                       // Verify if cipher is present.
+                       Cipher cipher = Cipher.getInstance(cryptoAlg);
+
                        // Standard names are not case-sensitive.
                        // http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html
                        String cryptoName = cryptoAlg.toUpperCase();
@@ -210,7 +226,7 @@ abstract class WalkEncryption {
                        if (!cryptoName.startsWith("PBE")) //$NON-NLS-1$
                                throw new GeneralSecurityException(JGitText.get().encryptionOnlyPBE);
 
-                       PBEKeySpec keySpec = new PBEKeySpec(key.toCharArray(), JETS3T_SALT, JETS3T_ITERATIONS, JETS3T_KEY_SIZE);
+                       PBEKeySpec keySpec = new PBEKeySpec(key.toCharArray(), SALT, ITERATIONS, KEY_SIZE);
                        secretKey = SecretKeyFactory.getInstance(algo).generateSecret(keySpec);
 
                        // Detect algorithms which require initialization vector.
@@ -229,12 +245,16 @@ abstract class WalkEncryption {
                                // https://bitbucket.org/jmurty/jets3t/raw/156c00eb160598c2e9937fd6873f00d3190e28ca/src/org/jets3t/service/security/EncryptionUtil.java
                                // http://cr.openjdk.java.net/~mullan/webrevs/ascarpin/webrev.00/raw_files/new/src/share/classes/com/sun/crypto/provider/PBES2Core.java
                                IvParameterSpec paramIV = new IvParameterSpec(ZERO_AES_IV);
-                               paramSpec = java8PBEParameterSpec(JETS3T_SALT, JETS3T_ITERATIONS, paramIV);
+                               paramSpec = java8PBEParameterSpec(SALT, ITERATIONS, paramIV);
                        } else {
                                // Strict legacy JetS3t V2 compatibility, with no IV support.
-                               paramSpec = java7PBEParameterSpec(JETS3T_SALT, JETS3T_ITERATIONS);
+                               paramSpec = java7PBEParameterSpec(SALT, ITERATIONS);
                        }
 
+                       // Verify if cipher + key are allowed by policy.
+                       cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
+                       cipher.doFinal();
+
                }
 
                @Override
@@ -271,4 +291,303 @@ abstract class WalkEncryption {
                        }
                }
        }
+
+       /** Encryption property names. */
+       interface Keys {
+               // Remote S3 meta: V1 algorithm name or V2 profile name.
+               String JGIT_PROFILE = "jgit-crypto-profile"; //$NON-NLS-1$
+
+               // Remote S3 meta: JGit encryption implementation version.
+               String JGIT_VERSION = "jgit-crypto-version"; //$NON-NLS-1$
+
+               // Remote S3 meta: base-64 encoded cipher algorithm parameters.
+               String JGIT_CONTEXT = "jgit-crypto-context"; //$NON-NLS-1$
+
+               // Amazon S3 connection configuration file profile property suffixes:
+               String X_ALGO = ".algo"; //$NON-NLS-1$
+               String X_KEY_ALGO = ".key.algo"; //$NON-NLS-1$
+               String X_KEY_SIZE = ".key.size"; //$NON-NLS-1$
+               String X_KEY_ITER = ".key.iter"; //$NON-NLS-1$
+               String X_KEY_SALT = ".key.salt"; //$NON-NLS-1$
+       }
+
+       /** Encryption constants and defaults. */
+       interface Vals {
+               // Compatibility defaults.
+               String DEFAULT_VERS = "0"; //$NON-NLS-1$
+               String DEFAULT_ALGO = JetS3tV2.ALGORITHM;
+               String DEFAULT_KEY_ALGO = JetS3tV2.ALGORITHM;
+               String DEFAULT_KEY_SIZE = Integer.toString(JetS3tV2.KEY_SIZE);
+               String DEFAULT_KEY_ITER = Integer.toString(JetS3tV2.ITERATIONS);
+               String DEFAULT_KEY_SALT = DatatypeConverter.printHexBinary(JetS3tV2.SALT);
+
+               String EMPTY = ""; //$NON-NLS-1$
+
+               // Match white space.
+               String REGEX_WS = "\\s+"; //$NON-NLS-1$
+
+               // Match PBE ciphers, i.e: PBEWithMD5AndDES
+               String REGEX_PBE = "(PBE).*(WITH).+(AND).+"; //$NON-NLS-1$
+
+               // Match transformation ciphers, i.e: AES/CBC/PKCS5Padding
+               String REGEX_TRANS = "(.+)/(.+)/(.+)"; //$NON-NLS-1$
+       }
+
+       static GeneralSecurityException securityError(String message) {
+               return new GeneralSecurityException(
+                               MessageFormat.format(JGitText.get().encryptionError, message));
+       }
+
+       /**
+        * Base implementation of JGit symmetric encryption. Supports V2 properties
+        * format.
+        */
+       static abstract class SymmetricEncryption extends WalkEncryption
+                       implements Keys, Vals {
+
+               /** Encryption profile, root name of group of related properties. */
+               final String profile;
+
+               /** Encryption version, reflects actual implementation class. */
+               final String version;
+
+               /** Full cipher algorithm name. */
+               final String cipherAlgo;
+
+               /** Cipher algorithm name for parameters lookup. */
+               final String paramsAlgo;
+
+               /** Generated secret key. */
+               final SecretKey secretKey;
+
+               SymmetricEncryption(Properties props) throws GeneralSecurityException {
+
+                       profile = props.getProperty(AmazonS3.Keys.CRYPTO_ALG);
+                       version = props.getProperty(AmazonS3.Keys.CRYPTO_VER);
+                       String pass = props.getProperty(AmazonS3.Keys.PASSWORD);
+
+                       cipherAlgo = props.getProperty(profile + X_ALGO, DEFAULT_ALGO);
+
+                       String keyAlgo = props.getProperty(profile + X_KEY_ALGO, DEFAULT_KEY_ALGO);
+                       String keySize = props.getProperty(profile + X_KEY_SIZE, DEFAULT_KEY_SIZE);
+                       String keyIter = props.getProperty(profile + X_KEY_ITER, DEFAULT_KEY_ITER);
+                       String keySalt = props.getProperty(profile + X_KEY_SALT, DEFAULT_KEY_SALT);
+
+                       // Verify if cipher is present.
+                       Cipher cipher = Cipher.getInstance(cipherAlgo);
+
+                       // Verify if key factory is present.
+                       SecretKeyFactory factory = SecretKeyFactory.getInstance(keyAlgo);
+
+                       final int size;
+                       try {
+                               size = Integer.parseInt(keySize);
+                       } catch (Exception e) {
+                               throw securityError(X_KEY_SIZE + EMPTY + keySize);
+                       }
+
+                       final int iter;
+                       try {
+                               iter = Integer.parseInt(keyIter);
+                       } catch (Exception e) {
+                               throw securityError(X_KEY_ITER + EMPTY + keyIter);
+                       }
+
+                       final byte[] salt;
+                       try {
+                               salt = DatatypeConverter
+                                               .parseHexBinary(keySalt.replaceAll(REGEX_WS, EMPTY));
+                       } catch (Exception e) {
+                               throw securityError(X_KEY_SALT + EMPTY + keySalt);
+                       }
+
+                       KeySpec keySpec = new PBEKeySpec(pass.toCharArray(), salt, iter, size);
+
+                       SecretKey keyBase = factory.generateSecret(keySpec);
+
+                       String name = cipherAlgo.toUpperCase();
+                       Matcher matcherPBE = Pattern.compile(REGEX_PBE).matcher(name);
+                       Matcher matcherTrans = Pattern.compile(REGEX_TRANS).matcher(name);
+                       if (matcherPBE.matches()) {
+                               paramsAlgo = cipherAlgo;
+                               secretKey = keyBase;
+                       } else if (matcherTrans.find()) {
+                               paramsAlgo = matcherTrans.group(1);
+                               secretKey = new SecretKeySpec(keyBase.getEncoded(), paramsAlgo);
+                       } else {
+                               throw new GeneralSecurityException(MessageFormat.format(
+                                               JGitText.get().unsupportedEncryptionAlgorithm,
+                                               cipherAlgo));
+                       }
+
+                       // Verify if cipher + key are allowed by policy.
+                       cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+                       cipher.doFinal();
+
+               }
+
+               // Shared state encrypt -> request.
+               volatile String context;
+
+               @Override
+               OutputStream encrypt(OutputStream output) throws IOException {
+                       try {
+                               Cipher cipher = Cipher.getInstance(cipherAlgo);
+                               cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+                               AlgorithmParameters params = cipher.getParameters();
+                               if (params == null) {
+                                       context = EMPTY;
+                               } else {
+                                       context = Base64.encodeBytes(params.getEncoded());
+                               }
+                               return new CipherOutputStream(output, cipher);
+                       } catch (Exception e) {
+                               throw error(e);
+                       }
+               }
+
+               @Override
+               void request(HttpURLConnection conn, String prefix) throws IOException {
+                       conn.setRequestProperty(prefix + JGIT_PROFILE, profile);
+                       conn.setRequestProperty(prefix + JGIT_VERSION, version);
+                       conn.setRequestProperty(prefix + JGIT_CONTEXT, context);
+                       // No cleanup:
+                       // single encrypt can be followed by several request
+                       // from the AmazonS3.putImpl() multiple retry attempts
+                       // context = null; // Cleanup encrypt -> request transition.
+                       // TODO re-factor AmazonS3.putImpl to be more transaction-like
+               }
+
+               // Shared state validate -> decrypt.
+               volatile Cipher decryptCipher;
+
+               @Override
+               void validate(HttpURLConnection conn, String prefix)
+                               throws IOException {
+                       String prof = conn.getHeaderField(prefix + JGIT_PROFILE);
+                       String vers = conn.getHeaderField(prefix + JGIT_VERSION);
+                       String cont = conn.getHeaderField(prefix + JGIT_CONTEXT);
+
+                       if (prof == null) {
+                               throw new IOException(MessageFormat
+                                               .format(JGitText.get().encryptionError, JGIT_PROFILE));
+                       }
+                       if (vers == null) {
+                               throw new IOException(MessageFormat
+                                               .format(JGitText.get().encryptionError, JGIT_VERSION));
+                       }
+                       if (cont == null) {
+                               throw new IOException(MessageFormat
+                                               .format(JGitText.get().encryptionError, JGIT_CONTEXT));
+                       }
+                       if (!profile.equals(prof)) {
+                               throw new IOException(MessageFormat.format(
+                                               JGitText.get().unsupportedEncryptionAlgorithm, prof));
+                       }
+                       if (!version.equals(vers)) {
+                               throw new IOException(MessageFormat.format(
+                                               JGitText.get().unsupportedEncryptionVersion, vers));
+                       }
+                       try {
+                               decryptCipher = Cipher.getInstance(cipherAlgo);
+                               if (cont.isEmpty()) {
+                                       decryptCipher.init(Cipher.DECRYPT_MODE, secretKey);
+                               } else {
+                                       AlgorithmParameters params = AlgorithmParameters
+                                                       .getInstance(paramsAlgo);
+                                       params.init(Base64.decode(cont));
+                                       decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, params);
+                               }
+                       } catch (Exception e) {
+                               throw error(e);
+                       }
+               }
+
+               @Override
+               InputStream decrypt(InputStream input) throws IOException {
+                       try {
+                               return new CipherInputStream(input, decryptCipher);
+                       } finally {
+                               decryptCipher = null; // Cleanup validate -> decrypt transition.
+                       }
+               }
+       }
+
+       /**
+        * Provides JetS3t-like encryption with AES support. Uses V1 connection file
+        * format. For reference, see: 'jgit-s3-connection-v-1.properties'.
+        */
+       static class JGitV1 extends SymmetricEncryption {
+
+               static final String VERSION = "1"; //$NON-NLS-1$
+
+               // Re-map connection properties V1 -> V2.
+               static Properties wrap(String algo, String pass) {
+                       Properties props = new Properties();
+                       props.put(AmazonS3.Keys.CRYPTO_ALG, algo);
+                       props.put(AmazonS3.Keys.CRYPTO_VER, VERSION);
+                       props.put(AmazonS3.Keys.PASSWORD, pass);
+                       props.put(algo + Keys.X_ALGO, algo);
+                       props.put(algo + Keys.X_KEY_ALGO, algo);
+                       props.put(algo + Keys.X_KEY_ITER, DEFAULT_KEY_ITER);
+                       props.put(algo + Keys.X_KEY_SIZE, DEFAULT_KEY_SIZE);
+                       props.put(algo + Keys.X_KEY_SALT, DEFAULT_KEY_SALT);
+                       return props;
+               }
+
+               JGitV1(String algo, String pass)
+                               throws GeneralSecurityException {
+                       super(wrap(algo, pass));
+                       String name = cipherAlgo.toUpperCase();
+                       Matcher matcherPBE = Pattern.compile(REGEX_PBE).matcher(name);
+                       if (!matcherPBE.matches())
+                               throw new GeneralSecurityException(
+                                               JGitText.get().encryptionOnlyPBE);
+               }
+
+       }
+
+       /**
+        * Supports both PBE and non-PBE algorithms. Uses V2 connection file format.
+        * For reference, see: 'jgit-s3-connection-v-2.properties'.
+        */
+       static class JGitV2 extends SymmetricEncryption {
+
+               static final String VERSION = "2"; //$NON-NLS-1$
+
+               JGitV2(Properties props)
+                               throws GeneralSecurityException {
+                       super(props);
+               }
+       }
+
+       /**
+        * Encryption factory.
+        *
+        * @param props
+        * @return instance
+        * @throws GeneralSecurityException
+        */
+       static WalkEncryption instance(Properties props)
+                       throws GeneralSecurityException {
+
+               String algo = props.getProperty(AmazonS3.Keys.CRYPTO_ALG, Vals.DEFAULT_ALGO);
+               String vers = props.getProperty(AmazonS3.Keys.CRYPTO_VER, Vals.DEFAULT_VERS);
+               String pass = props.getProperty(AmazonS3.Keys.PASSWORD);
+
+               if (pass == null) // Disable encryption.
+                       return WalkEncryption.NONE;
+
+               switch (vers) {
+               case Vals.DEFAULT_VERS:
+                       return new JetS3tV2(algo, pass);
+               case JGitV1.VERSION:
+                       return new JGitV1(algo, pass);
+               case JGitV2.VERSION:
+                       return new JGitV2(props);
+               default:
+                       throw new GeneralSecurityException(MessageFormat.format(
+                                       JGitText.get().unsupportedEncryptionVersion, vers));
+               }
+       }
 }