See previous attempt: https://git.eclipse.org/r/#/c/16674/ Here we preserve as much of JetS3t mode as possible while allowing to use new Java 8+ PBE algorithms such as PBEWithHmacSHA512AndAES_256 Summary of changes: * change pom.xml to control long tests * add WalkEncryptionTest.launch to run long tests * add AmazonS3.Keys to to normalize use of constants * change WalkEncryption to support AES in JetS3t mode * add WalkEncryptionTest to test remote encryption pipeline * add support for CI configuration for live Amazon S3 testing * add log4j based logging for tests in both Eclipse and Maven build To test locally, check out the review branch, then: * create amazon test configuration file * located your home dir: ${user.home} * named jgit-s3-config.properties * file format follows AmazonS3 connection settings file: accesskey = your-amazon-access-key secretkey = your-amazon-secret-key test.bucket = your-bucket-for-testing * finally: * run in Eclipse: WalkEncryptionTest.launch * or * run in Shell: mvn test --define test=WalkEncryptionTest Change-Id: I6f455fd9fb4eac261ca73d0bec6a4e7dae9f2e91 Signed-off-by: Andrei Pozolotin <andrei.pozolotin@gmail.com>tags/v4.2.0.201511101648-m1
/target | /target | ||||
/.project |
bin.includes = META-INF/,\ | bin.includes = META-INF/,\ | ||||
.,\ | .,\ | ||||
plugin.properties | plugin.properties | ||||
additional.bundles = org.apache.log4j |
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||||
<launchConfiguration type="org.eclipse.m2e.Maven2LaunchConfigurationType"> | |||||
<booleanAttribute key="M2_DEBUG_OUTPUT" value="false"/> | |||||
<stringAttribute key="M2_GOALS" value="test --define test=WalkEncryptionTest --define http_proxy=http://proxy:3128"/> | |||||
<booleanAttribute key="M2_NON_RECURSIVE" value="false"/> | |||||
<booleanAttribute key="M2_OFFLINE" value="false"/> | |||||
<stringAttribute key="M2_PROFILES" value=""/> | |||||
<listAttribute key="M2_PROPERTIES"/> | |||||
<stringAttribute key="M2_RUNTIME" value="EMBEDDED"/> | |||||
<booleanAttribute key="M2_SKIP_TESTS" value="false"/> | |||||
<intAttribute key="M2_THREADS" value="1"/> | |||||
<booleanAttribute key="M2_UPDATE_SNAPSHOTS" value="false"/> | |||||
<stringAttribute key="M2_USER_SETTINGS" value=""/> | |||||
<booleanAttribute key="M2_WORKSPACE_RESOLUTION" value="false"/> | |||||
<listAttribute key="org.eclipse.debug.ui.favoriteGroups"> | |||||
<listEntry value="org.eclipse.debug.ui.launchGroup.run"/> | |||||
</listAttribute> | |||||
<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/java-8-oracle"/> | |||||
<stringAttribute key="org.eclipse.jdt.launching.WORKING_DIRECTORY" value="${workspace_loc:/org.eclipse.jgit.test}"/> | |||||
</launchConfiguration> |
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||||
<launchConfiguration type="org.eclipse.m2e.Maven2LaunchConfigurationType"> | |||||
<booleanAttribute key="M2_DEBUG_OUTPUT" value="false"/> | |||||
<stringAttribute key="M2_GOALS" value="test --define test=WalkEncryptionTest --activate-profiles test.long"/> | |||||
<booleanAttribute key="M2_NON_RECURSIVE" value="false"/> | |||||
<booleanAttribute key="M2_OFFLINE" value="false"/> | |||||
<stringAttribute key="M2_PROFILES" value=""/> | |||||
<listAttribute key="M2_PROPERTIES"/> | |||||
<stringAttribute key="M2_RUNTIME" value="EMBEDDED"/> | |||||
<booleanAttribute key="M2_SKIP_TESTS" value="false"/> | |||||
<intAttribute key="M2_THREADS" value="1"/> | |||||
<booleanAttribute key="M2_UPDATE_SNAPSHOTS" value="false"/> | |||||
<stringAttribute key="M2_USER_SETTINGS" value=""/> | |||||
<booleanAttribute key="M2_WORKSPACE_RESOLUTION" value="false"/> | |||||
<listAttribute key="org.eclipse.debug.ui.favoriteGroups"> | |||||
<listEntry value="org.eclipse.debug.ui.launchGroup.run"/> | |||||
</listAttribute> | |||||
<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/java-8-oracle"/> | |||||
<stringAttribute key="org.eclipse.jdt.launching.WORKING_DIRECTORY" value="${workspace_loc:/org.eclipse.jgit.test}"/> | |||||
</launchConfiguration> |
<scope>test</scope> | <scope>test</scope> | ||||
</dependency> | </dependency> | ||||
<!-- Optional security provider for encryption tests. --> | |||||
<!-- See https://dev.eclipse.org/ipzilla/show_bug.cgi?id=9554 --> | |||||
<!-- See https://bugs.eclipse.org/bugs/show_bug.cgi?id=467064 --> | |||||
<dependency> | |||||
<groupId>org.bouncycastle</groupId> | |||||
<artifactId>bcprov-jdk15on</artifactId> | |||||
<version>1.52</version> | |||||
<scope>test</scope> | |||||
</dependency> | |||||
<dependency> | <dependency> | ||||
<groupId>org.hamcrest</groupId> | <groupId>org.hamcrest</groupId> | ||||
<artifactId>hamcrest-library</artifactId> | <artifactId>hamcrest-library</artifactId> | ||||
</dependency> | </dependency> | ||||
</dependencies> | </dependencies> | ||||
<profiles> | |||||
<!-- Profile provides a property which enables long running tests. --> | |||||
<profile> | |||||
<id>test.long</id> | |||||
<build> | |||||
<plugins> | |||||
<plugin> | |||||
<groupId>org.apache.maven.plugins</groupId> | |||||
<artifactId>maven-surefire-plugin</artifactId> | |||||
<configuration> | |||||
<argLine>-Djgit.test.long=true</argLine> | |||||
</configuration> | |||||
</plugin> | |||||
</plugins> | |||||
</build> | |||||
</profile> | |||||
</profiles> | |||||
<build> | <build> | ||||
<sourceDirectory>src/</sourceDirectory> | <sourceDirectory>src/</sourceDirectory> | ||||
<testSourceDirectory>tst/</testSourceDirectory> | <testSourceDirectory>tst/</testSourceDirectory> |
# | |||||
# See WalkEncryptionTest.java | |||||
# | |||||
# This file is a template for test configuration file used by WalkEncryptionTest. | |||||
# To be active, this file must have the following hard coded name: jgit-s3-config.properties | |||||
# To be active, this file must be discovered by WalkEncryptionTest from one of these locations: | |||||
# * ${user.home}/jgit-s3-config.properties | |||||
# * ${user.dir}/jgit-s3-config.properties | |||||
# * ${user.dir}/tst-rsrc/jgit-s3-config.properties | |||||
# When this file is missing, tests in WalkEncryptionTest will not run, only report a warning. | |||||
# | |||||
# | |||||
# WalkEncryptionTest requires amazon s3 test bucket setup. | |||||
# | |||||
# Test bucket setup instructions: | |||||
# | |||||
# Create IAM user: | |||||
# http://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html | |||||
# * user name: jgit.eclipse.org | |||||
# | |||||
# Configure IAM user S3 bucket access | |||||
# http://docs.aws.amazon.com/AmazonS3/latest/dev/example-policies-s3.html | |||||
# * attach S3 user policy to user account: jgit-s3-config.policy.user.json | |||||
# | |||||
# Create S3 bucket: | |||||
# http://docs.aws.amazon.com/AmazonS3/latest/gsg/CreatingABucket.html | |||||
# * bucket name: jgit.eclipse.org | |||||
# | |||||
# Configure S3 bucket source address/mask access: | |||||
# http://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html | |||||
# * attach bucket policy to the test bucket: jgit-s3-config.policy.bucket.json | |||||
# * verify that any required source address/mask is included in the bucket policy: | |||||
# * see https://wiki.eclipse.org/Hudson | |||||
# * see http://www.tcpiputils.com/browse/ip-address/198.41.30.200 | |||||
# * proxy.eclipse.org 198.41.30.0/24 | |||||
# * Andrei Pozolotin 67.175.188.187/32 | |||||
# | |||||
# Configure bucket 1 day expiration in object life cycle management: | |||||
# * https://docs.aws.amazon.com/AmazonS3/latest/dev/manage-lifecycle-using-console.html | |||||
# | |||||
# Test bucket name | |||||
test.bucket=jgit.eclipse.org | |||||
# IAM credentials for user jgit.eclipse.org | |||||
accesskey=AKIAIYWXB4ETREBRMZDQ | |||||
secretkey=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 |
{ | |||||
"Version": "2012-10-17", | |||||
"Statement": [ | |||||
{ | |||||
"Sid": "DenyAllButKnownSourceAddressWithMask", | |||||
"Effect": "Deny", | |||||
"Principal": "*", | |||||
"Action": "s3:*", | |||||
"Resource": "arn:aws:s3:::jgit.eclipse.org/*", | |||||
"Condition": { | |||||
"NotIpAddress": { | |||||
"aws:SourceIp": [ | |||||
"198.41.30.0/24", | |||||
"67.175.188.187/32" | |||||
] | |||||
} | |||||
} | |||||
} | |||||
] | |||||
} |
{ | |||||
"Version": "2012-10-17", | |||||
"Statement": [ | |||||
{ | |||||
"Sid": "BucketList", | |||||
"Effect": "Allow", | |||||
"Action": "s3:ListAllMyBuckets", | |||||
"Resource": [ | |||||
"arn:aws:s3:::jgit.eclipse.org" | |||||
] | |||||
}, | |||||
{ | |||||
"Sid": "BucketFullControl", | |||||
"Effect": "Allow", | |||||
"Action": [ | |||||
"s3:*" | |||||
], | |||||
"Resource": [ | |||||
"arn:aws:s3:::jgit.eclipse.org", | |||||
"arn:aws:s3:::jgit.eclipse.org/*" | |||||
] | |||||
} | |||||
] | |||||
} |
# Root logger option | |||||
log4j.rootLogger=INFO, stdout | |||||
# Direct log messages to stdout | |||||
log4j.appender.stdout=org.apache.log4j.ConsoleAppender | |||||
log4j.appender.stdout.Target=System.out | |||||
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout | |||||
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n |
emptyPathNotPermitted=Empty path not permitted. | emptyPathNotPermitted=Empty path not permitted. | ||||
emptyRef=Empty ref: {0} | emptyRef=Empty ref: {0} | ||||
encryptionError=Encryption error: {0} | encryptionError=Encryption error: {0} | ||||
encryptionOnlyPBE=Encryption error: only password-based encryption (PBE) algorithms are supported. | |||||
endOfFileInEscape=End of file in escape | endOfFileInEscape=End of file in escape | ||||
entryNotFoundByPath=Entry not found by path: {0} | entryNotFoundByPath=Entry not found by path: {0} | ||||
enumValueNotSupported2=Invalid value: {0}.{1}={2} | enumValueNotSupported2=Invalid value: {0}.{1}={2} |
/***/ public String emptyPathNotPermitted; | /***/ public String emptyPathNotPermitted; | ||||
/***/ public String emptyRef; | /***/ public String emptyRef; | ||||
/***/ public String encryptionError; | /***/ public String encryptionError; | ||||
/***/ public String encryptionOnlyPBE; | |||||
/***/ public String endOfFileInEscape; | /***/ public String endOfFileInEscape; | ||||
/***/ public String entryNotFoundByPath; | /***/ public String entryNotFoundByPath; | ||||
/***/ public String enumValueNotSupported2; | /***/ public String enumValueNotSupported2; |
import java.net.URL; | import java.net.URL; | ||||
import java.net.URLConnection; | import java.net.URLConnection; | ||||
import java.security.DigestOutputStream; | import java.security.DigestOutputStream; | ||||
import java.security.GeneralSecurityException; | |||||
import java.security.InvalidKeyException; | import java.security.InvalidKeyException; | ||||
import java.security.MessageDigest; | import java.security.MessageDigest; | ||||
import java.security.NoSuchAlgorithmException; | import java.security.NoSuchAlgorithmException; | ||||
import java.security.spec.InvalidKeySpecException; | |||||
import java.text.MessageFormat; | import java.text.MessageFormat; | ||||
import java.text.SimpleDateFormat; | import java.text.SimpleDateFormat; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
/** S3 Bucket Domain. */ | /** S3 Bucket Domain. */ | ||||
private final String domain; | private final String domain; | ||||
/** Property names used in amazon connection configuration file. */ | |||||
interface Keys { | |||||
String ACCESS_KEY = "accesskey"; //$NON-NLS-1$ | |||||
String SECRET_KEY = "secretkey"; //$NON-NLS-1$ | |||||
String PASSWORD = "password"; //$NON-NLS-1$ | |||||
String CRYPTO_ALG = "crypto.algorithm"; //$NON-NLS-1$ | |||||
String CRYPTO_VER = "crypto.version"; //$NON-NLS-1$ | |||||
String ACL = "acl"; //$NON-NLS-1$ | |||||
String DOMAIN = "domain"; //$NON-NLS-1$ | |||||
String HTTP_RETRY = "httpclient.retry-max"; //$NON-NLS-1$ | |||||
String TMP_DIR = "tmpdir"; //$NON-NLS-1$ | |||||
} | |||||
/** | /** | ||||
* Create a new S3 client for the supplied user information. | * Create a new S3 client for the supplied user information. | ||||
* <p> | * <p> | ||||
* | * | ||||
*/ | */ | ||||
public AmazonS3(final Properties props) { | public AmazonS3(final Properties props) { | ||||
domain = props.getProperty("domain", "s3.amazonaws.com"); //$NON-NLS-1$ //$NON-NLS-2$ | |||||
publicKey = props.getProperty("accesskey"); //$NON-NLS-1$ | |||||
domain = props.getProperty(Keys.DOMAIN, "s3.amazonaws.com"); //$NON-NLS-1$ | |||||
publicKey = props.getProperty(Keys.ACCESS_KEY); | |||||
if (publicKey == null) | if (publicKey == null) | ||||
throw new IllegalArgumentException(JGitText.get().missingAccesskey); | throw new IllegalArgumentException(JGitText.get().missingAccesskey); | ||||
final String secret = props.getProperty("secretkey"); //$NON-NLS-1$ | |||||
final String secret = props.getProperty(Keys.SECRET_KEY); | |||||
if (secret == null) | if (secret == null) | ||||
throw new IllegalArgumentException(JGitText.get().missingSecretkey); | throw new IllegalArgumentException(JGitText.get().missingSecretkey); | ||||
privateKey = new SecretKeySpec(Constants.encodeASCII(secret), HMAC); | privateKey = new SecretKeySpec(Constants.encodeASCII(secret), HMAC); | ||||
final String pacl = props.getProperty("acl", "PRIVATE"); //$NON-NLS-1$ //$NON-NLS-2$ | |||||
final String pacl = props.getProperty(Keys.ACL, "PRIVATE"); //$NON-NLS-1$ | |||||
if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) //$NON-NLS-1$ | if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) //$NON-NLS-1$ | ||||
acl = "private"; //$NON-NLS-1$ | acl = "private"; //$NON-NLS-1$ | ||||
else if (StringUtils.equalsIgnoreCase("PUBLIC", pacl)) //$NON-NLS-1$ | else if (StringUtils.equalsIgnoreCase("PUBLIC", pacl)) //$NON-NLS-1$ | ||||
throw new IllegalArgumentException("Invalid acl: " + pacl); //$NON-NLS-1$ | throw new IllegalArgumentException("Invalid acl: " + pacl); //$NON-NLS-1$ | ||||
try { | try { | ||||
final String cPas = props.getProperty("password"); //$NON-NLS-1$ | |||||
final String cPas = props.getProperty(Keys.PASSWORD); | |||||
if (cPas != null) { | if (cPas != null) { | ||||
String cAlg = props.getProperty("crypto.algorithm"); //$NON-NLS-1$ | |||||
String cAlg = props.getProperty(Keys.CRYPTO_ALG); | |||||
if (cAlg == null) | if (cAlg == null) | ||||
cAlg = "PBEWithMD5AndDES"; //$NON-NLS-1$ | |||||
encryption = new WalkEncryption.ObjectEncryptionV2(cAlg, cPas); | |||||
cAlg = WalkEncryption.ObjectEncryptionJetS3tV2.JETS3T_ALGORITHM; | |||||
encryption = new WalkEncryption.ObjectEncryptionJetS3tV2(cAlg, cPas); | |||||
} else { | } else { | ||||
encryption = WalkEncryption.NONE; | encryption = WalkEncryption.NONE; | ||||
} | } | ||||
} catch (InvalidKeySpecException e) { | |||||
throw new IllegalArgumentException(JGitText.get().invalidEncryption, e); | |||||
} catch (NoSuchAlgorithmException e) { | |||||
} catch (GeneralSecurityException e) { | |||||
throw new IllegalArgumentException(JGitText.get().invalidEncryption, e); | throw new IllegalArgumentException(JGitText.get().invalidEncryption, e); | ||||
} | } | ||||
maxAttempts = Integer.parseInt(props.getProperty( | |||||
"httpclient.retry-max", "3")); //$NON-NLS-1$ //$NON-NLS-2$ | |||||
maxAttempts = Integer | |||||
.parseInt(props.getProperty(Keys.HTTP_RETRY, "3")); //$NON-NLS-1$ | |||||
proxySelector = ProxySelector.getDefault(); | proxySelector = ProxySelector.getDefault(); | ||||
String tmp = props.getProperty("tmpdir"); //$NON-NLS-1$ | |||||
String tmp = props.getProperty(Keys.TMP_DIR); | |||||
tmpDir = tmp != null && tmp.length() > 0 ? new File(tmp) : null; | tmpDir = tmp != null && tmp.length() > 0 ? new File(tmp) : null; | ||||
} | } | ||||
import java.io.InputStream; | import java.io.InputStream; | ||||
import java.io.OutputStream; | import java.io.OutputStream; | ||||
import java.net.HttpURLConnection; | import java.net.HttpURLConnection; | ||||
import java.security.InvalidAlgorithmParameterException; | |||||
import java.security.InvalidKeyException; | |||||
import java.security.NoSuchAlgorithmException; | |||||
import java.security.spec.InvalidKeySpecException; | |||||
import java.security.GeneralSecurityException; | |||||
import java.security.spec.AlgorithmParameterSpec; | |||||
import java.text.MessageFormat; | import java.text.MessageFormat; | ||||
import javax.crypto.Cipher; | import javax.crypto.Cipher; | ||||
import javax.crypto.CipherInputStream; | import javax.crypto.CipherInputStream; | ||||
import javax.crypto.CipherOutputStream; | import javax.crypto.CipherOutputStream; | ||||
import javax.crypto.NoSuchPaddingException; | |||||
import javax.crypto.SecretKey; | import javax.crypto.SecretKey; | ||||
import javax.crypto.SecretKeyFactory; | import javax.crypto.SecretKeyFactory; | ||||
import javax.crypto.spec.IvParameterSpec; | |||||
import javax.crypto.spec.PBEKeySpec; | import javax.crypto.spec.PBEKeySpec; | ||||
import javax.crypto.spec.PBEParameterSpec; | import javax.crypto.spec.PBEParameterSpec; | ||||
abstract void request(HttpURLConnection u, String prefix); | abstract void request(HttpURLConnection u, String prefix); | ||||
abstract void validate(HttpURLConnection u, String p) throws IOException; | |||||
abstract void validate(HttpURLConnection u, String prefix) throws IOException; | |||||
protected void validateImpl(final HttpURLConnection u, final String p, | |||||
// TODO mixed ciphers | |||||
// consider permitting mixed ciphers to facilitate algorithm migration | |||||
// i.e. user keeps the password, but changes the algorithm | |||||
// then existing remote entries will still be readable | |||||
protected void validateImpl(final HttpURLConnection u, final String prefix, | |||||
final String version, final String name) throws IOException { | final String version, final String name) throws IOException { | ||||
String v; | String v; | ||||
v = u.getHeaderField(p + JETS3T_CRYPTO_VER); | |||||
v = u.getHeaderField(prefix + JETS3T_CRYPTO_VER); | |||||
if (v == null) | if (v == null) | ||||
v = ""; //$NON-NLS-1$ | v = ""; //$NON-NLS-1$ | ||||
if (!version.equals(v)) | if (!version.equals(v)) | ||||
throw new IOException(MessageFormat.format(JGitText.get().unsupportedEncryptionVersion, v)); | throw new IOException(MessageFormat.format(JGitText.get().unsupportedEncryptionVersion, v)); | ||||
v = u.getHeaderField(p + JETS3T_CRYPTO_ALG); | |||||
v = u.getHeaderField(prefix + JETS3T_CRYPTO_ALG); | |||||
if (v == null) | if (v == null) | ||||
v = ""; //$NON-NLS-1$ | v = ""; //$NON-NLS-1$ | ||||
if (!name.equals(v)) | |||||
throw new IOException(JGitText.get().unsupportedEncryptionAlgorithm + v); | |||||
// Standard names are not case-sensitive. | |||||
// http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html | |||||
if (!name.equalsIgnoreCase(v)) | |||||
throw new IOException(MessageFormat.format(JGitText.get().unsupportedEncryptionAlgorithm, v)); | |||||
} | } | ||||
IOException error(final Throwable why) { | IOException error(final Throwable why) { | ||||
} | } | ||||
@Override | @Override | ||||
void validate(final HttpURLConnection u, final String p) | |||||
void validate(final HttpURLConnection u, final String prefix) | |||||
throws IOException { | throws IOException { | ||||
validateImpl(u, p, "", ""); //$NON-NLS-1$ //$NON-NLS-2$ | |||||
validateImpl(u, prefix, "", ""); //$NON-NLS-1$ //$NON-NLS-2$ | |||||
} | } | ||||
@Override | @Override | ||||
} | } | ||||
} | } | ||||
static class ObjectEncryptionV2 extends WalkEncryption { | |||||
private static int ITERATION_COUNT = 5000; | |||||
// PBEParameterSpec factory for Java (version <= 7). | |||||
// Does not support AlgorithmParameterSpec. | |||||
static PBEParameterSpec java7PBEParameterSpec(byte[] salt, | |||||
int iterationCount) { | |||||
return new PBEParameterSpec(salt, iterationCount); | |||||
} | |||||
// PBEParameterSpec factory for Java (version >= 8). | |||||
// Adds support for AlgorithmParameterSpec. | |||||
static PBEParameterSpec java8PBEParameterSpec(byte[] salt, | |||||
int iterationCount, AlgorithmParameterSpec paramSpec) { | |||||
try { | |||||
@SuppressWarnings("boxing") | |||||
PBEParameterSpec instance = PBEParameterSpec.class | |||||
.getConstructor(byte[].class, int.class, | |||||
AlgorithmParameterSpec.class) | |||||
.newInstance(salt, iterationCount, paramSpec); | |||||
return instance; | |||||
} catch (Exception e) { | |||||
throw new RuntimeException(e); | |||||
} | |||||
} | |||||
// Current runtime version. | |||||
// https://docs.oracle.com/javase/7/docs/technotes/guides/versioning/spec/versioning2.html | |||||
static double javaVersion() { | |||||
return Double.parseDouble(System.getProperty("java.specification.version")); //$NON-NLS-1$ | |||||
} | |||||
/** | |||||
* JetS3t compatibility reference: <a href= | |||||
* "https://bitbucket.org/jmurty/jets3t/src/156c00eb160598c2e9937fd6873f00d3190e28ca/src/org/jets3t/service/security/EncryptionUtil.java"> | |||||
* EncryptionUtil.java</a> | |||||
* <p> | |||||
* Note: EncryptionUtil is inadequate: | |||||
* <li>EncryptionUtil.isCipherAvailableForUse checks encryption only which | |||||
* "always works", but in JetS3t both encryption and decryption use non-IV | |||||
* aware algorithm parameters for all PBE specs, which breaks in case of AES | |||||
* <li>that means that only non-IV algorithms will work round trip in | |||||
* JetS3t, such as PBEWithMD5AndDES and PBEWithSHAAndTwofish-CBC | |||||
* <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 final String JETS3T_VERSION = "2"; //$NON-NLS-1$ | |||||
static final String JETS3T_ALGORITHM = "PBEWithMD5AndDES"; //$NON-NLS-1$ | |||||
static final int JETS3T_ITERATIONS = 5000; | |||||
static final int JETS3T_KEY_SIZE = 32; | |||||
static final byte[] JETS3T_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 static byte[] salt = { (byte) 0xA4, (byte) 0x0B, (byte) 0xC8, | |||||
(byte) 0x34, (byte) 0xD6, (byte) 0x95, (byte) 0xF3, (byte) 0x13 }; | |||||
private final String cryptoVer = JETS3T_VERSION; | |||||
private final String algorithmName; | |||||
private final String cryptoAlg; | |||||
private final SecretKey skey; | |||||
private final SecretKey secretKey; | |||||
private final PBEParameterSpec aspec; | |||||
private final AlgorithmParameterSpec paramSpec; | |||||
ObjectEncryptionV2(final String algo, final String key) | |||||
throws InvalidKeySpecException, NoSuchAlgorithmException { | |||||
algorithmName = algo; | |||||
ObjectEncryptionJetS3tV2(final String algo, final String key) | |||||
throws GeneralSecurityException { | |||||
cryptoAlg = algo; | |||||
// 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); | |||||
secretKey = SecretKeyFactory.getInstance(algo).generateSecret(keySpec); | |||||
// Detect algorithms which require initialization vector. | |||||
boolean useIV = cryptoName.contains("AES"); //$NON-NLS-1$ | |||||
// PBEParameterSpec algorithm parameters are supported from Java 8. | |||||
boolean isJava8 = javaVersion() >= 1.8; | |||||
if (useIV && isJava8) { | |||||
// Support IV where possible: | |||||
// * since JCE provider uses random IV for PBE/AES | |||||
// * and there is no place to store dynamic IV in JetS3t V2 | |||||
// * we use static IV, and tolerate increased security risk | |||||
// TODO back port this change to JetS3t V2 | |||||
// See: | |||||
// 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); | |||||
} else { | |||||
// Strict legacy JetS3t V2 compatibility, with no IV support. | |||||
paramSpec = java7PBEParameterSpec(JETS3T_SALT, JETS3T_ITERATIONS); | |||||
} | |||||
final PBEKeySpec s; | |||||
s = new PBEKeySpec(key.toCharArray(), salt, ITERATION_COUNT, 32); | |||||
skey = SecretKeyFactory.getInstance(algo).generateSecret(s); | |||||
aspec = new PBEParameterSpec(salt, ITERATION_COUNT); | |||||
} | } | ||||
@Override | @Override | ||||
void request(final HttpURLConnection u, final String prefix) { | void request(final HttpURLConnection u, final String prefix) { | ||||
u.setRequestProperty(prefix + JETS3T_CRYPTO_VER, "2"); //$NON-NLS-1$ | |||||
u.setRequestProperty(prefix + JETS3T_CRYPTO_ALG, algorithmName); | |||||
u.setRequestProperty(prefix + JETS3T_CRYPTO_VER, cryptoVer); | |||||
u.setRequestProperty(prefix + JETS3T_CRYPTO_ALG, cryptoAlg); | |||||
} | } | ||||
@Override | @Override | ||||
void validate(final HttpURLConnection u, final String p) | |||||
void validate(final HttpURLConnection u, final String prefix) | |||||
throws IOException { | throws IOException { | ||||
validateImpl(u, p, "2", algorithmName); //$NON-NLS-1$ | |||||
validateImpl(u, prefix, cryptoVer, cryptoAlg); | |||||
} | } | ||||
@Override | @Override | ||||
OutputStream encrypt(final OutputStream os) throws IOException { | OutputStream encrypt(final OutputStream os) throws IOException { | ||||
try { | try { | ||||
final Cipher c = Cipher.getInstance(algorithmName); | |||||
c.init(Cipher.ENCRYPT_MODE, skey, aspec); | |||||
return new CipherOutputStream(os, c); | |||||
} catch (NoSuchAlgorithmException e) { | |||||
throw error(e); | |||||
} catch (NoSuchPaddingException e) { | |||||
throw error(e); | |||||
} catch (InvalidKeyException e) { | |||||
throw error(e); | |||||
} catch (InvalidAlgorithmParameterException e) { | |||||
final Cipher cipher = Cipher.getInstance(cryptoAlg); | |||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec); | |||||
return new CipherOutputStream(os, cipher); | |||||
} catch (GeneralSecurityException e) { | |||||
throw error(e); | throw error(e); | ||||
} | } | ||||
} | } | ||||
@Override | @Override | ||||
InputStream decrypt(final InputStream in) throws IOException { | InputStream decrypt(final InputStream in) throws IOException { | ||||
try { | try { | ||||
final Cipher c = Cipher.getInstance(algorithmName); | |||||
c.init(Cipher.DECRYPT_MODE, skey, aspec); | |||||
return new CipherInputStream(in, c); | |||||
} catch (NoSuchAlgorithmException e) { | |||||
throw error(e); | |||||
} catch (NoSuchPaddingException e) { | |||||
throw error(e); | |||||
} catch (InvalidKeyException e) { | |||||
throw error(e); | |||||
} catch (InvalidAlgorithmParameterException e) { | |||||
final Cipher cipher = Cipher.getInstance(cryptoAlg); | |||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, paramSpec); | |||||
return new CipherInputStream(in, cipher); | |||||
} catch (GeneralSecurityException e) { | |||||
throw error(e); | throw error(e); | ||||
} | } | ||||
} | } |