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 index 000000000..2402a4985 --- /dev/null +++ b/org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-0.properties @@ -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 index 000000000..d0d16118e --- /dev/null +++ b/org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-1.properties @@ -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 index 000000000..731b3247d --- /dev/null +++ b/org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-2.properties @@ -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 diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/WalkEncryptionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/WalkEncryptionTest.java index f2701cb41..042d7ba78 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/WalkEncryptionTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/WalkEncryptionTest.java @@ -73,6 +73,7 @@ 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 @@ */ @RunWith(Suite.class) @Suite.SuiteClasses({ // + WalkEncryptionTest.Required.class, // WalkEncryptionTest.MinimalSet.class, // WalkEncryptionTest.TestablePBE.class, // + WalkEncryptionTest.TestableTransformation.class, // }) public class WalkEncryptionTest { @@ -417,7 +420,12 @@ static String publicAddress() throws Exception { // https://www.bouncycastle.org/specifications.html // https://docs.oracle.com/javase/8/docs/technotes/guides/security/SunProviders.html static List cryptoCipherListPBE() { - return cryptoCipherList("(PBE).*(WITH).+(AND).+"); + 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 { @@ -437,25 +445,6 @@ static List cryptoCipherList(String regex) { return new ArrayList(target); } - /** - * 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(); - if (source.equalsIgnoreCase(target)) { - return true; - } - } - return false; - } - /** * Stream copy. * @@ -549,6 +538,51 @@ 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, 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 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; + } + } /** @@ -604,6 +638,20 @@ static void configCreate(String algorithm) throws Exception { 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 @@ static void remoteVerify() throws Exception { 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(); + 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 @@ static void remoteVerify() throws Exception { */ 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 @@ static boolean isAlgorithmAllowed(String algorithm) { } } + static boolean isAlgorithmAllowed(Properties props) { + try { + WalkEncryption.instance(props); + return true; + } catch (GeneralSecurityException e) { + return false; + } + } + /** * Verify round trip encryption. * @@ -736,6 +842,10 @@ static boolean isAlgorithmTestable(String algorithm) { && isAlgorithmAllowed(algorithm); } + static boolean isAlgorithmTestable(Properties props) { + return isAlgorithmPresent(props) && isAlgorithmAllowed(props); + } + /** * Log algorithm, provider, testability. * @@ -756,6 +866,26 @@ static void reportAlgorithmStatus(String algorithm) throws Exception { } } + 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 @@ static void reportLongTests() { public static void initialize() throws Exception { Transport.register(TransportAmazonS3.PROTO_S3); proxySetup(); + reportPolicy(); reportLongTests(); reportPublicAddress(); reportTestConfigPresent(); @@ -879,26 +1010,26 @@ public void tearDown() throws Exception { /** * 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 @@ void cryptoTest(String algorithm) throws Exception { } /** - * 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 void test_A1_ValidURI() throws Exception { @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 void test_A5_CryptoBouncyCastleCBC() throws Exception { @FixMethodOrder(MethodSorters.NAME_ASCENDING) public static class TestablePBE extends Base { - @Parameters(name = "Algorithm: {0}") - public static Collection algorimthmList() { - List source = cryptoCipherListPBE(); - List target = new ArrayList(); - for (String name : source) { - target.add(new Object[] { name }); - } - return target; + @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 algorithm; + final String profile; - public TestablePBE(String algorithm) { - this.algorithm = algorithm; + final String version; + + final String password = JGIT_PASS; + + public TestablePBE(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); + } + + } + + /** + * 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); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java index e55066a8b..0c8ee5d4b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java @@ -256,15 +256,7 @@ else if (StringUtils.equalsIgnoreCase("PUBLIC_READ", pacl)) //$NON-NLS-1$ 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); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java index e93a2af3e..fe03bdc86 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java @@ -47,9 +47,14 @@ 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.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 @@ static double javaVersion() { *
  • 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 @@ static class ObjectEncryptionJetS3tV2 extends 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 @@ static class ObjectEncryptionJetS3tV2 extends 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 @@ static class ObjectEncryptionJetS3tV2 extends 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 @@ static class ObjectEncryptionJetS3tV2 extends 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 @@ InputStream decrypt(final InputStream in) throws IOException { } } } + + /** 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)); + } + } }