WalkEncryption.java
- /*
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
- 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.eclipse.jgit.internal.JGitText;
- import org.eclipse.jgit.util.Base64;
- import org.eclipse.jgit.util.Hex;
- 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 = 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,
- Throwable cause) {
- GeneralSecurityException e = new GeneralSecurityException(
- MessageFormat.format(JGitText.get().encryptionError, message));
- e.initCause(cause);
- return e;
- }
- /**
- * Base implementation of JGit symmetric encryption. Supports V2 properties
- * format.
- */
- abstract static 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, e);
- }
- final int iter;
- try {
- iter = Integer.parseInt(keyIter);
- } catch (Exception e) {
- throw securityError(X_KEY_ITER + EMPTY + keyIter, e);
- }
- final byte[] salt;
- try {
- salt = Hex.decode(keySalt.replaceAll(REGEX_WS, EMPTY));
- } catch (Exception e) {
- throw securityError(X_KEY_SALT + EMPTY + keySalt, e);
- }
- 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));
- }
- }
- }