Browse Source

Adding JGitV1 and JGitV2 Walk Encryption

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>
stable-4.2
Andrei Pozolotin 9 years ago committed by Matthias Sohn
parent
commit
504e23b7a5
  1. 11
      org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-0.properties
  2. 14
      org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-1.properties
  3. 48
      org.eclipse.jgit.test/tst-rsrc/jgit-s3-connection-v-2.properties
  4. 336
      org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/WalkEncryptionTest.java
  5. 10
      org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java
  6. 349
      org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java

11
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

14
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

48
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

336
org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/WalkEncryptionTest.java

@ -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 });
@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);
}
return target;
}
final String algorithm;
/**
* 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 {
public TestablePBE(String algorithm) {
this.algorithm = algorithm;
@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);
}
@Test // Can take long time, needs activation.
public void test_B1_Crypto() throws Exception {
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());
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);
}
}

10
org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java

@ -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);
}

349
org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java

@ -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));
}
}
}

Loading…
Cancel
Save