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>
This commit is contained in:
parent
81810aff29
commit
504e23b7a5
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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<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 @@ static List<String> cryptoCipherList(String regex) {
|
|||
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 @@ 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<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 @@ 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<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 @@ 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<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 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<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 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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() {
|
|||
* <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 @@ 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue