WalkEncryption.java
/*
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.transport;
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.Locale;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
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();
static final String JETS3T_CRYPTO_VER = "jets3t-crypto-ver"; //$NON-NLS-1$
static final String JETS3T_CRYPTO_ALG = "jets3t-crypto-alg"; //$NON-NLS-1$
// Note: encrypt -> request state machine, step 1.
abstract OutputStream encrypt(OutputStream output) throws IOException;
// Note: encrypt -> request state machine, step 2.
abstract void request(HttpURLConnection conn, String prefix) throws IOException;
// 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;
// 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
/**
* Validate
*
* @param u
* a {@link java.net.HttpURLConnection} object.
* @param prefix
* a {@link java.lang.String} object.
* @param version
* a {@link java.lang.String} object.
* @param name
* a {@link java.lang.String} object.
* @throws java.io.IOException
* if any.
*/
protected void validateImpl(final HttpURLConnection u, final String prefix,
final String version, final String name) throws IOException {
String v;
v = u.getHeaderField(prefix + JETS3T_CRYPTO_VER);
if (v == null)
v = ""; //$NON-NLS-1$
if (!version.equals(v))
throw new IOException(MessageFormat.format(JGitText.get().unsupportedEncryptionVersion, v));
v = u.getHeaderField(prefix + JETS3T_CRYPTO_ALG);
if (v == null)
v = ""; //$NON-NLS-1$
// 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(Throwable why) {
return new IOException(MessageFormat
.format(JGitText.get().encryptionError,
why.getMessage()), why);
}
private static class NoEncryption extends WalkEncryption {
@Override
void request(HttpURLConnection u, String prefix) {
// Don't store any request properties.
}
@Override
void validate(HttpURLConnection u, String prefix)
throws IOException {
validateImpl(u, prefix, "", ""); //$NON-NLS-1$ //$NON-NLS-2$
}
@Override
InputStream decrypt(InputStream in) {
return in;
}
@Override
OutputStream encrypt(OutputStream os) {
return os;
}
}
/**
* 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 JetS3tV2 extends WalkEncryption {
static final String VERSION = "2"; //$NON-NLS-1$
static final String ALGORITHM = "PBEWithMD5AndDES"; //$NON-NLS-1$
static final int ITERATIONS = 5000;
static final int KEY_SIZE = 32;
static final byte[] SALT = { //
(byte) 0xA4, (byte) 0x0B, (byte) 0xC8, (byte) 0x34, //
(byte) 0xD6, (byte) 0x95, (byte) 0xF3, (byte) 0x13 //
};
// Size 16, see com.sun.crypto.provider.AESConstants.AES_BLOCK_SIZE
static final byte[] ZERO_AES_IV = new byte[16];
private static final String CRYPTO_VER = VERSION;
private final String cryptoAlg;
private final SecretKey secretKey;
private final AlgorithmParameterSpec paramSpec;
JetS3tV2(final String algo, final String key)
throws GeneralSecurityException {
cryptoAlg = algo;
// Verify if cipher is present.
Cipher cipher = InsecureCipherFactory.create(cryptoAlg);
// Standard names are not case-sensitive.
// http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html
String cryptoName = cryptoAlg.toUpperCase(Locale.ROOT);
if (!cryptoName.startsWith("PBE")) //$NON-NLS-1$
throw new GeneralSecurityException(JGitText.get().encryptionOnlyPBE);
PBEKeySpec keySpec = new PBEKeySpec(key.toCharArray(), SALT, ITERATIONS, 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.
if (useIV) {
// 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 = new PBEParameterSpec(SALT, ITERATIONS, paramIV);
} else {
// Strict legacy JetS3t V2 compatibility, with no IV support.
paramSpec = new PBEParameterSpec(SALT, ITERATIONS);
}
// Verify if cipher + key are allowed by policy.
cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
cipher.doFinal();
}
@Override
void request(HttpURLConnection u, String prefix) {
u.setRequestProperty(prefix + JETS3T_CRYPTO_VER, CRYPTO_VER);
u.setRequestProperty(prefix + JETS3T_CRYPTO_ALG, cryptoAlg);
}
@Override
void validate(HttpURLConnection u, String prefix)
throws IOException {
validateImpl(u, prefix, CRYPTO_VER, cryptoAlg);
}
@Override
OutputStream encrypt(OutputStream os) throws IOException {
try {
final Cipher cipher = InsecureCipherFactory.create(cryptoAlg);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
return new CipherOutputStream(os, cipher);
} catch (GeneralSecurityException e) {
throw error(e);
}
}
@Override
InputStream decrypt(InputStream in) throws IOException {
try {
final Cipher cipher = InsecureCipherFactory.create(cryptoAlg);
cipher.init(Cipher.DECRYPT_MODE, secretKey, paramSpec);
return new CipherInputStream(in, cipher);
} catch (GeneralSecurityException e) {
throw error(e);
}
}
}
/** 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 = InsecureCipherFactory.create(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(Locale.ROOT);
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 = InsecureCipherFactory.create(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 = InsecureCipherFactory.create(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(Locale.ROOT);
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));
}
}
}