/*
 * 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 org.bouncycastle.util.encoders.Hex;
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
Params:
Throws:
/** * 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: EncryptionUtil.java

Note: EncryptionUtil is inadequate:

  • 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
  • that means that only non-IV algorithms will work round trip in JetS3t, such as PBEWithMD5AndDES and PBEWithSHAAndTwofish-CBC
  • any AES based algorithms such as "PBE...With...And...AES" will not work, since they need proper IV setup
  • /** * 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.
    /** 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.
    /** 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 = Hex.toHexString(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.
    /** * 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.
    /** Encryption profile, root name of group of related properties. */
    final String profile;
    Encryption version, reflects actual implementation class.
    /** Encryption version, reflects actual implementation class. */
    final String version;
    Full cipher algorithm name.
    /** Full cipher algorithm name. */
    final String cipherAlgo;
    Cipher algorithm name for parameters lookup.
    /** Cipher algorithm name for parameters lookup. */
    final String paramsAlgo;
    Generated secret key.
    /** 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 = Hex.decode(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'.
    /** * 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'.
    /** * 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.
    Params:
    • props –
    Throws:
    Returns:instance
    /** * 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)); } } }