| /* | |
| * 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: | |
| * <ul> | |
| * <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 | |
| * </ul> | |
| */ | |
| 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 | |
| * configuration properties | |
| * @return instance this object | |
| * @throws GeneralSecurityException | |
| * if generic security failure occurred | |
| */ | |
| 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)); | |
| } | |
| } | |
| } |