org.syncany.crypto.CipherUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.syncany.crypto.CipherUtil.java

Source

/*
 * Syncany, www.syncany.org
 * Copyright (C) 2011-2015 Philipp C. Heckel <philipp.heckel@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.syncany.crypto;

import static org.syncany.crypto.CipherParams.CRYPTO_PROVIDER;
import static org.syncany.crypto.CipherParams.CRYPTO_PROVIDER_ID;
import static org.syncany.crypto.CipherParams.KEY_DERIVATION_DIGEST;
import static org.syncany.crypto.CipherParams.KEY_DERIVATION_INFO;
import static org.syncany.crypto.CipherParams.MASTER_KEY_DERIVATION_FUNCTION;
import static org.syncany.crypto.CipherParams.MASTER_KEY_DERIVATION_ROUNDS;
import static org.syncany.crypto.CipherParams.MASTER_KEY_SALT_SIZE;
import static org.syncany.crypto.CipherParams.MASTER_KEY_SIZE;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
import org.bouncycastle.crypto.params.HKDFParameters;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

/**
 * The cipher utility provides functions to create a master key using PBKDF2,
 * a derived key using SHA256, and to create a {@link Cipher} from a derived key.
 * It furthermore offers a method to programmatically enable the unlimited strength
 * crypto policies.
 *
 * @author Philipp C. Heckel <philipp.heckel@gmail.com>
 */
public class CipherUtil {
    private static final Logger logger = Logger.getLogger(CipherUtil.class.getSimpleName());

    /**
     * Chars from A-Z / a-z to be used in randomly generated passwords.
     *
     * <p><b>Note:</b> This string cannot contain numbers, to prevent breaking
     * of the vector clock format.
     */
    private static final String ALPHABETIC_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    private static AtomicBoolean initialized = new AtomicBoolean(false);
    private static AtomicBoolean unlimitedStrengthEnabled = new AtomicBoolean(false);
    private static SecureRandom secureRandom = new SecureRandom();

    static {
        init();
    }

    /**
     * Initializes the crypto provider ("Bouncy Castle") and tests whether the unlimited
     * strength policy has been enabled. Unlimited crypto allows for stronger crypto algorithms
     * such as AES-256 or Twofish-256.
     *
     * <p>The method is called in the <tt>static</tt> block of this class and hence initialized
     * whenever then class is used.
     *
     * @see <a href="www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html">Java Cryptography Extension (JCE) Unlimited Strength</a>
     */
    public static synchronized void init() {
        if (!initialized.get()) {
            // Bouncy Castle
            if (Security.getProvider(CRYPTO_PROVIDER_ID) == null) {
                Security.addProvider(CRYPTO_PROVIDER);
            }

            // Unlimited strength
            try {
                unlimitedStrengthEnabled.set(Cipher.getMaxAllowedKeyLength("AES") > 128);
            } catch (NoSuchAlgorithmException e) {
                logger.log(Level.FINE, "No such transformation found", e);
                unlimitedStrengthEnabled.set(false);
            }

            initialized.set(true);
        }
    }

    /**
     * Attempts to programmatically enable the unlimited strength Java crypto extension
     * using the reflection API.
     *
     * <p>This class tries to set the property <tt>isRestricted</tt> of the class
     * <tt>javax.crypto.JceSecurity</tt> to <tt>false</tt> -- effectively disabling
     * the artificial limitations (and the disallowed algorithms).
     *
     * <p><b>Note</b>: Be aware that enabling the unlimited strength extension needs to
     * be acknowledged by the end-user to avoid legal issues!
     *
     * @throws CipherException If the unlimited strength policy cannot be enabled.
     * @see <a href="www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html">Java Cryptography Extension (JCE) Unlimited Strength</a>
     */
    public static void enableUnlimitedStrength() throws CipherException {
        if (!unlimitedStrengthEnabled.get()) {
            logger.log(Level.FINE, "- Enabling unlimited strength/crypto ...");

            try {
                Field field = Class.forName("javax.crypto.JceSecurity").getDeclaredField("isRestricted");

                field.setAccessible(true);
                field.set(null, false);
            } catch (Exception e) {
                throw new CipherException(e);
            }
        }
    }

    /**
     * Creates a random array of bytes using the default {@link SecureRandom} implementation
     * of the currently active JVM. The returned array can be used as a basis for secret keys,
     * IVs or salts.
     *
     * @param size Size of the returned array (in bytes)
     * @return Returns a random byte array (using a secure pseudo random generator)
     */
    public static byte[] createRandomArray(int size) {
        byte[] randomByteArray = new byte[size];
        secureRandom.nextBytes(randomByteArray);

        return randomByteArray;
    }

    /**
     * Generates a random string the given length. Only uses characters
     * A-Z/a-z (in order to always create valid serialized vector clock representations).
     */
    public static String createRandomAlphabeticString(int size) {
        StringBuilder sb = new StringBuilder(size);

        for (int i = 0; i < size; i++) {
            sb.append(ALPHABETIC_CHARS.charAt(secureRandom.nextInt(ALPHABETIC_CHARS.length())));
        }

        return sb.toString();
    }

    /**
     * Creates a derived key from the given {@link SecretKey} an input salt and wraps the key in
     * a {@link SecretKeySpec} using the given {@link CipherSpec}.
     *
     * <p>This method simply uses the {@link #createDerivedKey(byte[], byte[], String, int) createDerivedKey()}
     * method using the encoded input key and the algorithm and key size given by the cipher spec.
     *
     * @param inputKey The source key to derive the new key from
     * @param inputSalt Input salt used to generate the new key (a non-secret random value!)
     * @param outputCipherSpec Defines the algorithm and key size of the new output key
     * @return Returns a derived key (including the given input salt)
     */
    public static SaltedSecretKey createDerivedKey(SecretKey inputKey, byte[] inputSalt,
            CipherSpec outputCipherSpec)
            throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException {

        return createDerivedKey(inputKey.getEncoded(), inputSalt, outputCipherSpec.getAlgorithm(),
                outputCipherSpec.getKeySize());
    }

    /**
     * Creates a derived key from the given input key material (raw byte array) and an input salt
     * and wraps the key in  a {@link SecretKeySpec} using the given output key algorithm and output
     * key size.
     *
     * <p>The algorithm used to derive the new key from the input key material (IKM) is the
     * <b>HMAC-based Extract-and-Expand Key Derivation Function (HKDF)</b> (see
     * <a href="http://tools.ietf.org/html/rfc5869">RFC 5869</a>)
     *
     * @param inputKeyMaterial The input key material as raw data bytes, e.g. determined from {@link SecretKey#getEncoded()}
     * @param inputSalt Input salt used to generate the new key (a non-secret random value!)
     * @param outputKeyAlgorithm Defines the algorithm of the new output key (for {@link SecretKeySpec#getAlgorithm()})
     * @param outputKeySize Defines the key size of the new output key
     * @return Returns a new pseudorandom key derived from the input key material using HKDF
     * @see <a href="http://tools.ietf.org/html/rfc5869">RFC 5869</a>
     */
    public static SaltedSecretKey createDerivedKey(byte[] inputKeyMaterial, byte[] inputSalt,
            String outputKeyAlgorithm, int outputKeySize)
            throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException {

        HKDFBytesGenerator hkdf = new HKDFBytesGenerator(KEY_DERIVATION_DIGEST);
        hkdf.init(new HKDFParameters(inputKeyMaterial, inputSalt, KEY_DERIVATION_INFO));

        byte[] derivedKey = new byte[outputKeySize / 8];
        hkdf.generateBytes(derivedKey, 0, derivedKey.length);

        return toSaltedSecretKey(derivedKey, inputSalt, outputKeyAlgorithm);
    }

    public static SecretKey toSecretKey(byte[] secretKeyBytes, String algorithm) {
        String plainAlgorithm = (algorithm.indexOf('/') != -1) ? algorithm.substring(0, algorithm.indexOf('/'))
                : algorithm;
        SecretKey secretKey = new SecretKeySpec(secretKeyBytes, plainAlgorithm);

        return secretKey;
    }

    public static SaltedSecretKey toSaltedSecretKey(byte[] secretKeyBytes, byte[] saltBytes, String algorithm) {
        return new SaltedSecretKey(toSecretKey(secretKeyBytes, algorithm), saltBytes);
    }

    public static SaltedSecretKey createMasterKey(String password) throws CipherException {
        byte[] salt = createRandomArray(MASTER_KEY_SALT_SIZE / 8);
        return createMasterKey(password, salt);
    }

    public static SaltedSecretKey createMasterKey(String password, byte[] salt) throws CipherException {
        try {
            logger.log(Level.FINE, "- Creating secret key using {0} with {1} rounds, key size {2} bit ...",
                    new Object[] { MASTER_KEY_DERIVATION_FUNCTION, MASTER_KEY_DERIVATION_ROUNDS, MASTER_KEY_SIZE });

            SecretKeyFactory factory = SecretKeyFactory.getInstance(MASTER_KEY_DERIVATION_FUNCTION);
            KeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), salt, MASTER_KEY_DERIVATION_ROUNDS,
                    MASTER_KEY_SIZE);
            SecretKey masterKey = factory.generateSecret(pbeKeySpec);

            return new SaltedSecretKey(masterKey, salt);
        } catch (Exception e) {
            throw new CipherException(e);
        }
    }

    public static boolean isEncrypted(File file) throws IOException {
        byte[] actualMagic = new byte[MultiCipherOutputStream.STREAM_MAGIC.length];

        RandomAccessFile rFile = new RandomAccessFile(file, "r");
        rFile.read(actualMagic);
        rFile.close();

        return Arrays.equals(actualMagic, MultiCipherOutputStream.STREAM_MAGIC);
    }

    public static void encrypt(InputStream plaintextInputStream, OutputStream ciphertextOutputStream,
            List<CipherSpec> cipherSpecs, SaltedSecretKey masterKey) throws CipherException {

        try {
            CipherSession cipherSession = new CipherSession(masterKey);
            OutputStream multiCipherOutputStream = new MultiCipherOutputStream(ciphertextOutputStream, cipherSpecs,
                    cipherSession);

            int read = -1;
            byte[] buffer = new byte[4096];

            while (-1 != (read = plaintextInputStream.read(buffer))) {
                multiCipherOutputStream.write(buffer, 0, read);
            }

            plaintextInputStream.close();
            multiCipherOutputStream.close();
        } catch (IOException e) {
            throw new CipherException(e);
        }
    }

    public static byte[] encrypt(InputStream plaintextInputStream, List<CipherSpec> cipherSuites,
            SaltedSecretKey masterKey) throws CipherException {
        ByteArrayOutputStream ciphertextOutputStream = new ByteArrayOutputStream();
        encrypt(plaintextInputStream, ciphertextOutputStream, cipherSuites, masterKey);

        return ciphertextOutputStream.toByteArray();
    }

    public static byte[] decrypt(InputStream fromInputStream, SaltedSecretKey masterKey) throws CipherException {
        try {
            CipherSession cipherSession = new CipherSession(masterKey);
            MultiCipherInputStream multiCipherInputStream = new MultiCipherInputStream(fromInputStream,
                    cipherSession);
            ByteArrayOutputStream plaintextOutputStream = new ByteArrayOutputStream();

            int read = -1;
            byte[] buffer = new byte[4096];

            while (-1 != (read = multiCipherInputStream.read(buffer))) {
                plaintextOutputStream.write(buffer, 0, read);
            }

            multiCipherInputStream.close();
            plaintextOutputStream.close();

            return plaintextOutputStream.toByteArray();
        } catch (IOException e) {
            throw new CipherException(e);
        }
    }

    /**
     * Generates a 2048-bit RSA key pair.
     */
    public static KeyPair generateRsaKeyPair()
            throws NoSuchAlgorithmException, CipherException, NoSuchProviderException {
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance(CipherParams.CERTIFICATE_KEYPAIR_ALGORITHM,
                CipherParams.CRYPTO_PROVIDER_ID);
        keyGen.initialize(CipherParams.CERTIFICATE_KEYPAIR_SIZE, secureRandom);

        return keyGen.generateKeyPair();
    }

    /**
     * Generates a self-signed certificate, given a public/private key pair.
     *
     * @see <a href="https://code.google.com/p/gitblit/source/browse/src/com/gitblit/MakeCertificate.java?r=88598bb2f779b73479512d818c675dea8fa72138">Original source of this method</a>
     */
    public static X509Certificate generateSelfSignedCertificate(String commonName, KeyPair keyPair)
            throws OperatorCreationException, CertificateException, InvalidKeyException, NoSuchAlgorithmException,
            NoSuchProviderException, SignatureException {

        // Certificate CN, O and OU
        X500NameBuilder builder = new X500NameBuilder(BCStyle.INSTANCE);

        builder.addRDN(BCStyle.CN, commonName);
        builder.addRDN(BCStyle.O, CipherParams.CERTIFICATE_ORGANIZATION);
        builder.addRDN(BCStyle.OU, CipherParams.CERTIFICATE_ORGUNIT);

        // Dates and serial
        Date notBefore = new Date(System.currentTimeMillis() - 1 * 24 * 60 * 60 * 1000L);
        Date notAfter = new Date(System.currentTimeMillis() + 5 * 365 * 24 * 60 * 60 * 1000L);
        BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());

        // Issuer and subject (identical, because self-signed)
        X500Name issuer = builder.build();
        X500Name subject = issuer;

        X509v3CertificateBuilder certificateGenerator = new JcaX509v3CertificateBuilder(issuer, serial, notBefore,
                notAfter, subject, keyPair.getPublic());

        ContentSigner signatureGenerator = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
                .setProvider(CipherParams.CRYPTO_PROVIDER).build(keyPair.getPrivate());

        X509Certificate certificate = new JcaX509CertificateConverter().setProvider(CipherParams.CRYPTO_PROVIDER)
                .getCertificate(certificateGenerator.build(signatureGenerator));

        certificate.checkValidity(new Date());
        certificate.verify(certificate.getPublicKey());

        return certificate;
    }

    /**
     * Creates an SSL context, given a key store and a trust store.
     */
    public static SSLContext createSSLContext(KeyStore keyStore, KeyStore trustStore) throws Exception {
        try {
            // Server key and certificate
            KeyManagerFactory keyManagerFactory = KeyManagerFactory
                    .getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(keyStore, new char[0]);

            KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();

            // Trusted certificates
            TrustManagerFactory trustManagerFactory = TrustManagerFactory
                    .getInstance(KeyManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(trustStore);

            TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

            // Create SSL context
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(keyManagers, trustManagers, null);

            return sslContext;
        } catch (Exception e) {
            throw new Exception("Unable to initialize SSL context", e);
        }
    }
}