--- /dev/null
+#
+# 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
--- /dev/null
+#
+# 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
--- /dev/null
+#
+# 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
import java.util.TreeSet;
import java.util.UUID;
+import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import org.apache.log4j.Logger;
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({ //
+ WalkEncryptionTest.Required.class, //
WalkEncryptionTest.MinimalSet.class, //
WalkEncryptionTest.TestablePBE.class, //
+ WalkEncryptionTest.TestableTransformation.class, //
})
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 {
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.
*
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;
+ }
+
}
/**
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.
*
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.
*
*/
static boolean isAlgorithmAllowed(String algorithm) {
try {
- WalkEncryption crypto = new WalkEncryption.ObjectEncryptionJetS3tV2(
+ WalkEncryption crypto = new WalkEncryption.JetS3tV2(
algorithm, JGIT_PASS);
verifyCrypto(crypto);
return true;
}
}
+ static boolean isAlgorithmAllowed(Properties props) {
+ try {
+ WalkEncryption.instance(props);
+ return true;
+ } catch (GeneralSecurityException e) {
+ return false;
+ }
+ }
+
/**
* Verify round trip encryption.
*
&& isAlgorithmAllowed(algorithm);
}
+ static boolean isAlgorithmTestable(Properties props) {
+ return isAlgorithmPresent(props) && isAlgorithmAllowed(props);
+ }
+
/**
* Log algorithm, provider, testability.
*
}
}
+ 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.
*
public static void initialize() throws Exception {
Transport.register(TransportAmazonS3.PROTO_S3);
proxySetup();
+ reportPolicy();
reportLongTests();
reportPublicAddress();
reportTestConfigPresent();
/**
* 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();
}
/**
- * 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 {
@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);
}
}
@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);
}
}
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);
}
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;
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();
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
* <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 //
};
// 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;
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();
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.
// 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
}
}
}
+
+ /** 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));
+ }
+ }
}