WalkEncryption.java

  1. /*
  2.  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.transport;

  11. import java.io.IOException;
  12. import java.io.InputStream;
  13. import java.io.OutputStream;
  14. import java.net.HttpURLConnection;
  15. import java.security.AlgorithmParameters;
  16. import java.security.GeneralSecurityException;
  17. import java.security.spec.AlgorithmParameterSpec;
  18. import java.security.spec.KeySpec;
  19. import java.text.MessageFormat;
  20. import java.util.Locale;
  21. import java.util.Properties;
  22. import java.util.regex.Matcher;
  23. import java.util.regex.Pattern;

  24. import javax.crypto.Cipher;
  25. import javax.crypto.CipherInputStream;
  26. import javax.crypto.CipherOutputStream;
  27. import javax.crypto.SecretKey;
  28. import javax.crypto.SecretKeyFactory;
  29. import javax.crypto.spec.IvParameterSpec;
  30. import javax.crypto.spec.PBEKeySpec;
  31. import javax.crypto.spec.PBEParameterSpec;
  32. import javax.crypto.spec.SecretKeySpec;

  33. import org.eclipse.jgit.internal.JGitText;
  34. import org.eclipse.jgit.util.Base64;
  35. import org.eclipse.jgit.util.Hex;

  36. abstract class WalkEncryption {
  37.     static final WalkEncryption NONE = new NoEncryption();

  38.     static final String JETS3T_CRYPTO_VER = "jets3t-crypto-ver"; //$NON-NLS-1$

  39.     static final String JETS3T_CRYPTO_ALG = "jets3t-crypto-alg"; //$NON-NLS-1$

  40.     // Note: encrypt -> request state machine, step 1.
  41.     abstract OutputStream encrypt(OutputStream output) throws IOException;

  42.     // Note: encrypt -> request state machine, step 2.
  43.     abstract void request(HttpURLConnection conn, String prefix) throws IOException;

  44.     // Note: validate -> decrypt state machine, step 1.
  45.     abstract void validate(HttpURLConnection conn, String prefix) throws IOException;

  46.     // Note: validate -> decrypt state machine, step 2.
  47.     abstract InputStream decrypt(InputStream input) throws IOException;


  48.     // TODO mixed ciphers
  49.     // consider permitting mixed ciphers to facilitate algorithm migration
  50.     // i.e. user keeps the password, but changes the algorithm
  51.     // then existing remote entries will still be readable
  52.     /**
  53.      * Validate
  54.      *
  55.      * @param u
  56.      *            a {@link java.net.HttpURLConnection} object.
  57.      * @param prefix
  58.      *            a {@link java.lang.String} object.
  59.      * @param version
  60.      *            a {@link java.lang.String} object.
  61.      * @param name
  62.      *            a {@link java.lang.String} object.
  63.      * @throws java.io.IOException
  64.      *             if any.
  65.      */
  66.     protected void validateImpl(final HttpURLConnection u, final String prefix,
  67.             final String version, final String name) throws IOException {
  68.         String v;

  69.         v = u.getHeaderField(prefix + JETS3T_CRYPTO_VER);
  70.         if (v == null)
  71.             v = ""; //$NON-NLS-1$
  72.         if (!version.equals(v))
  73.             throw new IOException(MessageFormat.format(JGitText.get().unsupportedEncryptionVersion, v));

  74.         v = u.getHeaderField(prefix + JETS3T_CRYPTO_ALG);
  75.         if (v == null)
  76.             v = ""; //$NON-NLS-1$
  77.         // Standard names are not case-sensitive.
  78.         // http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html
  79.         if (!name.equalsIgnoreCase(v))
  80.             throw new IOException(MessageFormat.format(JGitText.get().unsupportedEncryptionAlgorithm, v));
  81.     }

  82.     IOException error(Throwable why) {
  83.         return new IOException(MessageFormat
  84.                 .format(JGitText.get().encryptionError,
  85.                 why.getMessage()), why);
  86.     }

  87.     private static class NoEncryption extends WalkEncryption {
  88.         @Override
  89.         void request(HttpURLConnection u, String prefix) {
  90.             // Don't store any request properties.
  91.         }

  92.         @Override
  93.         void validate(HttpURLConnection u, String prefix)
  94.                 throws IOException {
  95.             validateImpl(u, prefix, "", ""); //$NON-NLS-1$ //$NON-NLS-2$
  96.         }

  97.         @Override
  98.         InputStream decrypt(InputStream in) {
  99.             return in;
  100.         }

  101.         @Override
  102.         OutputStream encrypt(OutputStream os) {
  103.             return os;
  104.         }
  105.     }

  106.     /**
  107.      * JetS3t compatibility reference: <a href=
  108.      * "https://bitbucket.org/jmurty/jets3t/src/156c00eb160598c2e9937fd6873f00d3190e28ca/src/org/jets3t/service/security/EncryptionUtil.java">
  109.      * EncryptionUtil.java</a>
  110.      * <p>
  111.      * Note: EncryptionUtil is inadequate:
  112.      * <li>EncryptionUtil.isCipherAvailableForUse checks encryption only which
  113.      * "always works", but in JetS3t both encryption and decryption use non-IV
  114.      * aware algorithm parameters for all PBE specs, which breaks in case of AES
  115.      * <li>that means that only non-IV algorithms will work round trip in
  116.      * JetS3t, such as PBEWithMD5AndDES and PBEWithSHAAndTwofish-CBC
  117.      * <li>any AES based algorithms such as "PBE...With...And...AES" will not
  118.      * work, since they need proper IV setup
  119.      */
  120.     static class JetS3tV2 extends WalkEncryption {

  121.         static final String VERSION = "2"; //$NON-NLS-1$

  122.         static final String ALGORITHM = "PBEWithMD5AndDES"; //$NON-NLS-1$

  123.         static final int ITERATIONS = 5000;

  124.         static final int KEY_SIZE = 32;

  125.         static final byte[] SALT = { //
  126.                 (byte) 0xA4, (byte) 0x0B, (byte) 0xC8, (byte) 0x34, //
  127.                 (byte) 0xD6, (byte) 0x95, (byte) 0xF3, (byte) 0x13 //
  128.         };

  129.         // Size 16, see com.sun.crypto.provider.AESConstants.AES_BLOCK_SIZE
  130.         static final byte[] ZERO_AES_IV = new byte[16];

  131.         private static final String CRYPTO_VER = VERSION;

  132.         private final String cryptoAlg;

  133.         private final SecretKey secretKey;

  134.         private final AlgorithmParameterSpec paramSpec;

  135.         JetS3tV2(final String algo, final String key)
  136.                 throws GeneralSecurityException {
  137.             cryptoAlg = algo;

  138.             // Verify if cipher is present.
  139.             Cipher cipher = InsecureCipherFactory.create(cryptoAlg);

  140.             // Standard names are not case-sensitive.
  141.             // http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html
  142.             String cryptoName = cryptoAlg.toUpperCase(Locale.ROOT);

  143.             if (!cryptoName.startsWith("PBE")) //$NON-NLS-1$
  144.                 throw new GeneralSecurityException(JGitText.get().encryptionOnlyPBE);

  145.             PBEKeySpec keySpec = new PBEKeySpec(key.toCharArray(), SALT, ITERATIONS, KEY_SIZE);
  146.             secretKey = SecretKeyFactory.getInstance(algo).generateSecret(keySpec);

  147.             // Detect algorithms which require initialization vector.
  148.             boolean useIV = cryptoName.contains("AES"); //$NON-NLS-1$

  149.             // PBEParameterSpec algorithm parameters are supported from Java 8.
  150.             if (useIV) {
  151.                 // Support IV where possible:
  152.                 // * since JCE provider uses random IV for PBE/AES
  153.                 // * and there is no place to store dynamic IV in JetS3t V2
  154.                 // * we use static IV, and tolerate increased security risk
  155.                 // TODO back port this change to JetS3t V2
  156.                 // See:
  157.                 // https://bitbucket.org/jmurty/jets3t/raw/156c00eb160598c2e9937fd6873f00d3190e28ca/src/org/jets3t/service/security/EncryptionUtil.java
  158.                 // http://cr.openjdk.java.net/~mullan/webrevs/ascarpin/webrev.00/raw_files/new/src/share/classes/com/sun/crypto/provider/PBES2Core.java
  159.                 IvParameterSpec paramIV = new IvParameterSpec(ZERO_AES_IV);
  160.                 paramSpec = new PBEParameterSpec(SALT, ITERATIONS, paramIV);
  161.             } else {
  162.                 // Strict legacy JetS3t V2 compatibility, with no IV support.
  163.                 paramSpec = new PBEParameterSpec(SALT, ITERATIONS);
  164.             }

  165.             // Verify if cipher + key are allowed by policy.
  166.             cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
  167.             cipher.doFinal();
  168.         }

  169.         @Override
  170.         void request(HttpURLConnection u, String prefix) {
  171.             u.setRequestProperty(prefix + JETS3T_CRYPTO_VER, CRYPTO_VER);
  172.             u.setRequestProperty(prefix + JETS3T_CRYPTO_ALG, cryptoAlg);
  173.         }

  174.         @Override
  175.         void validate(HttpURLConnection u, String prefix)
  176.                 throws IOException {
  177.             validateImpl(u, prefix, CRYPTO_VER, cryptoAlg);
  178.         }

  179.         @Override
  180.         OutputStream encrypt(OutputStream os) throws IOException {
  181.             try {
  182.                 final Cipher cipher = InsecureCipherFactory.create(cryptoAlg);
  183.                 cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
  184.                 return new CipherOutputStream(os, cipher);
  185.             } catch (GeneralSecurityException e) {
  186.                 throw error(e);
  187.             }
  188.         }

  189.         @Override
  190.         InputStream decrypt(InputStream in) throws IOException {
  191.             try {
  192.                 final Cipher cipher = InsecureCipherFactory.create(cryptoAlg);
  193.                 cipher.init(Cipher.DECRYPT_MODE, secretKey, paramSpec);
  194.                 return new CipherInputStream(in, cipher);
  195.             } catch (GeneralSecurityException e) {
  196.                 throw error(e);
  197.             }
  198.         }
  199.     }

  200.     /** Encryption property names. */
  201.     interface Keys {
  202.         // Remote S3 meta: V1 algorithm name or V2 profile name.
  203.         String JGIT_PROFILE = "jgit-crypto-profile"; //$NON-NLS-1$

  204.         // Remote S3 meta: JGit encryption implementation version.
  205.         String JGIT_VERSION = "jgit-crypto-version"; //$NON-NLS-1$

  206.         // Remote S3 meta: base-64 encoded cipher algorithm parameters.
  207.         String JGIT_CONTEXT = "jgit-crypto-context"; //$NON-NLS-1$

  208.         // Amazon S3 connection configuration file profile property suffixes:
  209.         String X_ALGO = ".algo"; //$NON-NLS-1$
  210.         String X_KEY_ALGO = ".key.algo"; //$NON-NLS-1$
  211.         String X_KEY_SIZE = ".key.size"; //$NON-NLS-1$
  212.         String X_KEY_ITER = ".key.iter"; //$NON-NLS-1$
  213.         String X_KEY_SALT = ".key.salt"; //$NON-NLS-1$
  214.     }

  215.     /** Encryption constants and defaults. */
  216.     interface Vals {
  217.         // Compatibility defaults.
  218.         String DEFAULT_VERS = "0"; //$NON-NLS-1$
  219.         String DEFAULT_ALGO = JetS3tV2.ALGORITHM;
  220.         String DEFAULT_KEY_ALGO = JetS3tV2.ALGORITHM;
  221.         String DEFAULT_KEY_SIZE = Integer.toString(JetS3tV2.KEY_SIZE);
  222.         String DEFAULT_KEY_ITER = Integer.toString(JetS3tV2.ITERATIONS);
  223.         String DEFAULT_KEY_SALT = Hex.toHexString(JetS3tV2.SALT);

  224.         String EMPTY = ""; //$NON-NLS-1$

  225.         // Match white space.
  226.         String REGEX_WS = "\\s+"; //$NON-NLS-1$

  227.         // Match PBE ciphers, i.e: PBEWithMD5AndDES
  228.         String REGEX_PBE = "(PBE).*(WITH).+(AND).+"; //$NON-NLS-1$

  229.         // Match transformation ciphers, i.e: AES/CBC/PKCS5Padding
  230.         String REGEX_TRANS = "(.+)/(.+)/(.+)"; //$NON-NLS-1$
  231.     }

  232.     static GeneralSecurityException securityError(String message,
  233.             Throwable cause) {
  234.         GeneralSecurityException e = new GeneralSecurityException(
  235.                 MessageFormat.format(JGitText.get().encryptionError, message));
  236.         e.initCause(cause);
  237.         return e;
  238.     }

  239.     /**
  240.      * Base implementation of JGit symmetric encryption. Supports V2 properties
  241.      * format.
  242.      */
  243.     abstract static class SymmetricEncryption extends WalkEncryption
  244.             implements Keys, Vals {

  245.         /** Encryption profile, root name of group of related properties. */
  246.         final String profile;

  247.         /** Encryption version, reflects actual implementation class. */
  248.         final String version;

  249.         /** Full cipher algorithm name. */
  250.         final String cipherAlgo;

  251.         /** Cipher algorithm name for parameters lookup. */
  252.         final String paramsAlgo;

  253.         /** Generated secret key. */
  254.         final SecretKey secretKey;

  255.         SymmetricEncryption(Properties props) throws GeneralSecurityException {

  256.             profile = props.getProperty(AmazonS3.Keys.CRYPTO_ALG);
  257.             version = props.getProperty(AmazonS3.Keys.CRYPTO_VER);
  258.             String pass = props.getProperty(AmazonS3.Keys.PASSWORD);

  259.             cipherAlgo = props.getProperty(profile + X_ALGO, DEFAULT_ALGO);

  260.             String keyAlgo = props.getProperty(profile + X_KEY_ALGO, DEFAULT_KEY_ALGO);
  261.             String keySize = props.getProperty(profile + X_KEY_SIZE, DEFAULT_KEY_SIZE);
  262.             String keyIter = props.getProperty(profile + X_KEY_ITER, DEFAULT_KEY_ITER);
  263.             String keySalt = props.getProperty(profile + X_KEY_SALT, DEFAULT_KEY_SALT);

  264.             // Verify if cipher is present.
  265.             Cipher cipher = InsecureCipherFactory.create(cipherAlgo);

  266.             // Verify if key factory is present.
  267.             SecretKeyFactory factory = SecretKeyFactory.getInstance(keyAlgo);

  268.             final int size;
  269.             try {
  270.                 size = Integer.parseInt(keySize);
  271.             } catch (Exception e) {
  272.                 throw securityError(X_KEY_SIZE + EMPTY + keySize, e);
  273.             }

  274.             final int iter;
  275.             try {
  276.                 iter = Integer.parseInt(keyIter);
  277.             } catch (Exception e) {
  278.                 throw securityError(X_KEY_ITER + EMPTY + keyIter, e);
  279.             }

  280.             final byte[] salt;
  281.             try {
  282.                 salt = Hex.decode(keySalt.replaceAll(REGEX_WS, EMPTY));
  283.             } catch (Exception e) {
  284.                 throw securityError(X_KEY_SALT + EMPTY + keySalt, e);
  285.             }

  286.             KeySpec keySpec = new PBEKeySpec(pass.toCharArray(), salt, iter, size);

  287.             SecretKey keyBase = factory.generateSecret(keySpec);

  288.             String name = cipherAlgo.toUpperCase(Locale.ROOT);
  289.             Matcher matcherPBE = Pattern.compile(REGEX_PBE).matcher(name);
  290.             Matcher matcherTrans = Pattern.compile(REGEX_TRANS).matcher(name);
  291.             if (matcherPBE.matches()) {
  292.                 paramsAlgo = cipherAlgo;
  293.                 secretKey = keyBase;
  294.             } else if (matcherTrans.find()) {
  295.                 paramsAlgo = matcherTrans.group(1);
  296.                 secretKey = new SecretKeySpec(keyBase.getEncoded(), paramsAlgo);
  297.             } else {
  298.                 throw new GeneralSecurityException(MessageFormat.format(
  299.                         JGitText.get().unsupportedEncryptionAlgorithm,
  300.                         cipherAlgo));
  301.             }

  302.             // Verify if cipher + key are allowed by policy.
  303.             cipher.init(Cipher.ENCRYPT_MODE, secretKey);
  304.             cipher.doFinal();

  305.         }

  306.         // Shared state encrypt -> request.
  307.         volatile String context;

  308.         @Override
  309.         OutputStream encrypt(OutputStream output) throws IOException {
  310.             try {
  311.                 Cipher cipher = InsecureCipherFactory.create(cipherAlgo);
  312.                 cipher.init(Cipher.ENCRYPT_MODE, secretKey);
  313.                 AlgorithmParameters params = cipher.getParameters();
  314.                 if (params == null) {
  315.                     context = EMPTY;
  316.                 } else {
  317.                     context = Base64.encodeBytes(params.getEncoded());
  318.                 }
  319.                 return new CipherOutputStream(output, cipher);
  320.             } catch (Exception e) {
  321.                 throw error(e);
  322.             }
  323.         }

  324.         @Override
  325.         void request(HttpURLConnection conn, String prefix) throws IOException {
  326.             conn.setRequestProperty(prefix + JGIT_PROFILE, profile);
  327.             conn.setRequestProperty(prefix + JGIT_VERSION, version);
  328.             conn.setRequestProperty(prefix + JGIT_CONTEXT, context);
  329.             // No cleanup:
  330.             // single encrypt can be followed by several request
  331.             // from the AmazonS3.putImpl() multiple retry attempts
  332.             // context = null; // Cleanup encrypt -> request transition.
  333.             // TODO re-factor AmazonS3.putImpl to be more transaction-like
  334.         }

  335.         // Shared state validate -> decrypt.
  336.         volatile Cipher decryptCipher;

  337.         @Override
  338.         void validate(HttpURLConnection conn, String prefix)
  339.                 throws IOException {
  340.             String prof = conn.getHeaderField(prefix + JGIT_PROFILE);
  341.             String vers = conn.getHeaderField(prefix + JGIT_VERSION);
  342.             String cont = conn.getHeaderField(prefix + JGIT_CONTEXT);

  343.             if (prof == null) {
  344.                 throw new IOException(MessageFormat
  345.                         .format(JGitText.get().encryptionError, JGIT_PROFILE));
  346.             }
  347.             if (vers == null) {
  348.                 throw new IOException(MessageFormat
  349.                         .format(JGitText.get().encryptionError, JGIT_VERSION));
  350.             }
  351.             if (cont == null) {
  352.                 throw new IOException(MessageFormat
  353.                         .format(JGitText.get().encryptionError, JGIT_CONTEXT));
  354.             }
  355.             if (!profile.equals(prof)) {
  356.                 throw new IOException(MessageFormat.format(
  357.                         JGitText.get().unsupportedEncryptionAlgorithm, prof));
  358.             }
  359.             if (!version.equals(vers)) {
  360.                 throw new IOException(MessageFormat.format(
  361.                         JGitText.get().unsupportedEncryptionVersion, vers));
  362.             }
  363.             try {
  364.                 decryptCipher = InsecureCipherFactory.create(cipherAlgo);
  365.                 if (cont.isEmpty()) {
  366.                     decryptCipher.init(Cipher.DECRYPT_MODE, secretKey);
  367.                 } else {
  368.                     AlgorithmParameters params = AlgorithmParameters
  369.                             .getInstance(paramsAlgo);
  370.                     params.init(Base64.decode(cont));
  371.                     decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, params);
  372.                 }
  373.             } catch (Exception e) {
  374.                 throw error(e);
  375.             }
  376.         }

  377.         @Override
  378.         InputStream decrypt(InputStream input) throws IOException {
  379.             try {
  380.                 return new CipherInputStream(input, decryptCipher);
  381.             } finally {
  382.                 decryptCipher = null; // Cleanup validate -> decrypt transition.
  383.             }
  384.         }
  385.     }

  386.     /**
  387.      * Provides JetS3t-like encryption with AES support. Uses V1 connection file
  388.      * format. For reference, see: 'jgit-s3-connection-v-1.properties'.
  389.      */
  390.     static class JGitV1 extends SymmetricEncryption {

  391.         static final String VERSION = "1"; //$NON-NLS-1$

  392.         // Re-map connection properties V1 -> V2.
  393.         static Properties wrap(String algo, String pass) {
  394.             Properties props = new Properties();
  395.             props.put(AmazonS3.Keys.CRYPTO_ALG, algo);
  396.             props.put(AmazonS3.Keys.CRYPTO_VER, VERSION);
  397.             props.put(AmazonS3.Keys.PASSWORD, pass);
  398.             props.put(algo + Keys.X_ALGO, algo);
  399.             props.put(algo + Keys.X_KEY_ALGO, algo);
  400.             props.put(algo + Keys.X_KEY_ITER, DEFAULT_KEY_ITER);
  401.             props.put(algo + Keys.X_KEY_SIZE, DEFAULT_KEY_SIZE);
  402.             props.put(algo + Keys.X_KEY_SALT, DEFAULT_KEY_SALT);
  403.             return props;
  404.         }

  405.         JGitV1(String algo, String pass)
  406.                 throws GeneralSecurityException {
  407.             super(wrap(algo, pass));
  408.             String name = cipherAlgo.toUpperCase(Locale.ROOT);
  409.             Matcher matcherPBE = Pattern.compile(REGEX_PBE).matcher(name);
  410.             if (!matcherPBE.matches())
  411.                 throw new GeneralSecurityException(
  412.                         JGitText.get().encryptionOnlyPBE);
  413.         }

  414.     }

  415.     /**
  416.      * Supports both PBE and non-PBE algorithms. Uses V2 connection file format.
  417.      * For reference, see: 'jgit-s3-connection-v-2.properties'.
  418.      */
  419.     static class JGitV2 extends SymmetricEncryption {

  420.         static final String VERSION = "2"; //$NON-NLS-1$

  421.         JGitV2(Properties props)
  422.                 throws GeneralSecurityException {
  423.             super(props);
  424.         }
  425.     }

  426.     /**
  427.      * Encryption factory.
  428.      *
  429.      * @param props
  430.      * @return instance
  431.      * @throws GeneralSecurityException
  432.      */
  433.     static WalkEncryption instance(Properties props)
  434.             throws GeneralSecurityException {

  435.         String algo = props.getProperty(AmazonS3.Keys.CRYPTO_ALG, Vals.DEFAULT_ALGO);
  436.         String vers = props.getProperty(AmazonS3.Keys.CRYPTO_VER, Vals.DEFAULT_VERS);
  437.         String pass = props.getProperty(AmazonS3.Keys.PASSWORD);

  438.         if (pass == null) // Disable encryption.
  439.             return WalkEncryption.NONE;

  440.         switch (vers) {
  441.         case Vals.DEFAULT_VERS:
  442.             return new JetS3tV2(algo, pass);
  443.         case JGitV1.VERSION:
  444.             return new JGitV1(algo, pass);
  445.         case JGitV2.VERSION:
  446.             return new JGitV2(props);
  447.         default:
  448.             throw new GeneralSecurityException(MessageFormat.format(
  449.                     JGitText.get().unsupportedEncryptionVersion, vers));
  450.         }
  451.     }
  452. }