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