org.ccnx.ccn.impl.security.keystore.AESKeyStoreSpi.java Source code

Java tutorial

Introduction

Here is the source code for org.ccnx.ccn.impl.security.keystore.AESKeyStoreSpi.java

Source

/*
 * Part of the CCNx Java Library.
 *
 * Copyright (C) 2013 Palo Alto Research Center, Inc.
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation. 
 * This library 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
 * Lesser General Public License for more details. You should have received
 * a copy of the GNU Lesser General Public License along with this library;
 * if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301 USA.
 */
package org.ccnx.ccn.impl.security.keystore;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.KeyStoreSpi;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1OutputStream;
import org.bouncycastle.asn1.DERInteger;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.ccnx.ccn.impl.security.crypto.util.OIDLookup;
import org.ccnx.ccn.impl.support.Log;
import org.ccnx.ccn.impl.support.Tuple;

/**
 * This is a specialized keystore for storing symmetric keys. We looked at PKCS #11 for this but decided 
 * against it for now because industry doesn't seem to be standardizing around it - at least not yet, and
 * standard support for it is somewhat sketchy at this point.
 * 
 * The keystore can be used for only one key at a time and is located by naming it with a suffix
 * created from the key's digest.
 * 
 * Following is the formula for the KeyStore
 * 
 * Let P=passphrase
 * Let PT = symmetric key to store
 * Let IV = random 16-bytes
 *
 * aesK = HMAC-SHA256(P, '\0')
 * macK = HMAC-SHA256(P, '\1')
 * AES256-CBC(IV, key, PT) - performs AES256 in CBC mode
 *
 * SK = IV || AES256-CBC(IV, aesK, PT) || HMAC-SHA256(macK, AES256-CBC(IV, aesK, PT))
 *
 * SK is the symmetric keystore ciphertext
 * 
 * ASN1 encoded KeyStore = Version || Key algorithm OID || SK
 */
public class AESKeyStoreSpi extends KeyStoreSpi {

    public static final int VERSION = 1;
    public static final String TYPE = "CCN_AES";
    public static final String MAC_ALGORITHM = "HMAC-SHA256"; // XXX Should these be settable?
    public static final String AES_ALGORITHM = "AES";
    public static final String AES_CRYPTO_ALGORITHM = "AES/CBC/PKCS5Padding";

    public static final int IV_SIZE = 16;

    public static final Random _random = new SecureRandom();

    public static Mac _macKeyMac;
    public static Mac _AESKeyMac;
    public static String _AESKeyAlgorithm = MAC_ALGORITHM;

    protected static DERInteger _version = new DERInteger(VERSION);

    protected byte[] _id = null;
    protected KeyStore.Entry _ourEntry = null;
    protected DERObjectIdentifier _oid = null;

    /*
     * Convert from a key algorithm to a size for an encrypted key
     * XXX might be some better way to do this but I don't know what it is...
     */
    private static Map<String, Integer> _k2Size = new HashMap<String, Integer>();

    static {
        _k2Size.put("SHA256", 48);
        try {
            _macKeyMac = Mac.getInstance(MAC_ALGORITHM);
            int maxKeyLen = Cipher.getMaxAllowedKeyLength(AES_ALGORITHM);
            if (maxKeyLen < 160)
                _AESKeyAlgorithm = "HMACMD5";
            else if (maxKeyLen < 256)
                _AESKeyAlgorithm = /* "HMACSHA1"; */ "HMACMD5"; // HMACSHA1 doesn't seem to work for some reason...
            _AESKeyMac = Mac.getInstance(_AESKeyAlgorithm);
        } catch (NoSuchAlgorithmException e) {
            Log.severe("Couldn't initialize for keystore due to: {0}", e.getMessage());
        }
    }

    /*
     * TODO
     * As far as I know we don't need to do most of this stuff. If we discover its needed, it will be filled in later
     */
    @Override
    public Enumeration<String> engineAliases() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public boolean engineContainsAlias(String arg0) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public void engineDeleteEntry(String arg0) throws KeyStoreException {
        // TODO Auto-generated method stub

    }

    @Override
    public Certificate engineGetCertificate(String arg0) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public String engineGetCertificateAlias(Certificate arg0) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Certificate[] engineGetCertificateChain(String arg0) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Date engineGetCreationDate(String arg0) {
        // TODO Auto-generated method stub
        return null;
    }

    /**
     * Create a new entry for the keystore if needed. Since there is only 1 key in the keystore
     * we only ever return the single entry.
     */
    public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter protParam) {
        if (null == _ourEntry) {
            if (null != _id) {
                SecretKeySpec sks = new SecretKeySpec(_id, MAC_ALGORITHM);
                _ourEntry = new KeyStore.SecretKeyEntry(sks);
            }
        }
        return _ourEntry;
    }

    @Override
    public Key engineGetKey(String arg0, char[] arg1) throws NoSuchAlgorithmException, UnrecoverableKeyException {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public boolean engineIsCertificateEntry(String arg0) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean engineIsKeyEntry(String arg0) {
        // TODO Auto-generated method stub
        return false;
    }

    /**
     * Load in the key from the keystore file
     */
    @Override
    public void engineLoad(InputStream stream, char[] password)
            throws IOException, NoSuchAlgorithmException, CertificateException {
        if (null == stream)
            return;
        if (null != _id)
            return; // We already have the key so don't need to reload it
        ASN1InputStream ais = new ASN1InputStream(stream);
        DERSequence ds = (DERSequence) ais.readObject();
        DERInteger version = (DERInteger) ds.getObjectAt(0);
        if (version.getValue().intValue() != VERSION)
            throw new IOException("Unsupported AESKeyStore version: " + version.getValue().intValue());
        _oid = (DERObjectIdentifier) ds.getObjectAt(1);
        String keyAlgorithm = OIDLookup.getDigestName(_oid.toString());
        int aeslen = keyAlgorithmToCipherSize(keyAlgorithm);
        ASN1OctetString os = (ASN1OctetString) ds.getObjectAt(2);
        byte[] cryptoData = os.getOctets();
        int checkLength = cryptoData.length - (IV_SIZE + aeslen);
        if (checkLength <= 0)
            throw new IOException("Corrupted keystore");
        byte[] iv = new byte[IV_SIZE];
        System.arraycopy(cryptoData, 0, iv, 0, iv.length);
        Tuple<SecretKeySpec, SecretKeySpec> keys = initializeForAES(password);
        try {
            Cipher cipher = Cipher.getInstance(AES_CRYPTO_ALGORITHM);
            IvParameterSpec ivspec = new IvParameterSpec(iv);
            cipher.init(Cipher.DECRYPT_MODE, keys.first(), ivspec);
            byte[] cryptBytes = new byte[aeslen];
            System.arraycopy(cryptoData, IV_SIZE, cryptBytes, 0, cryptBytes.length);
            _id = cipher.doFinal(cryptBytes);
            byte[] checkbuf = new byte[IV_SIZE + cryptBytes.length];
            System.arraycopy(iv, 0, checkbuf, 0, IV_SIZE);
            System.arraycopy(cryptBytes, 0, checkbuf, IV_SIZE, cryptBytes.length);
            byte[] check = new byte[checkLength];
            System.arraycopy(cryptoData, IV_SIZE + aeslen, check, 0, checkLength);
            _macKeyMac.init(keys.second());
            byte[] hmac = _macKeyMac.doFinal(checkbuf);
            if (!Arrays.equals(hmac, check))
                throw new IOException("Bad signature in AES keystore");
        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    @Override
    public void engineSetCertificateEntry(String arg0, Certificate arg1) throws KeyStoreException {
        // TODO Auto-generated method stub

    }

    @Override
    public void engineSetKeyEntry(String arg0, byte[] arg1, Certificate[] arg2) throws KeyStoreException {
    }

    @Override
    public void engineSetKeyEntry(String name, Key key, char[] arg2, Certificate[] arg3) throws KeyStoreException {
        _id = key.getEncoded();
        String oid = OIDLookup.getDigestOID(key.getAlgorithm());
        if (null == oid)
            throw new KeyStoreException("Not a Mac algorithm we recognize: " + key.getAlgorithm());
        _oid = new DERObjectIdentifier(oid);
    }

    @Override
    public int engineSize() {
        // TODO Auto-generated method stub
        return 0;
    }

    /**
     * Store the key from _id into a keystore file
     */
    @Override
    public void engineStore(OutputStream stream, char[] password)
            throws IOException, NoSuchAlgorithmException, CertificateException {
        if (null == _id)
            throw new IOException("Key not entered yet");
        ASN1OutputStream aos = new ASN1OutputStream(stream);
        Tuple<SecretKeySpec, SecretKeySpec> keys = initializeForAES(password);
        try {
            byte[] iv = new byte[IV_SIZE];
            _random.nextBytes(iv);
            byte[] aesCBC = null;
            Cipher cipher = Cipher.getInstance(AES_CRYPTO_ALGORITHM);
            IvParameterSpec ivspec = new IvParameterSpec(iv);
            cipher.init(Cipher.ENCRYPT_MODE, keys.first(), ivspec);
            aesCBC = cipher.doFinal(_id);
            _macKeyMac.init(keys.second());
            byte[] checkbuf = new byte[iv.length + aesCBC.length];
            System.arraycopy(iv, 0, checkbuf, 0, iv.length);
            System.arraycopy(aesCBC, 0, checkbuf, iv.length, aesCBC.length);
            byte[] part3 = _macKeyMac.doFinal(checkbuf);
            // TODO might be a better way to do this but am not sure how
            // (and its not really that important anyway)
            byte[] asn1buf = new byte[iv.length + aesCBC.length + part3.length];
            System.arraycopy(checkbuf, 0, asn1buf, 0, checkbuf.length);
            System.arraycopy(part3, 0, asn1buf, iv.length + aesCBC.length, part3.length);
            ASN1OctetString os = new DEROctetString(asn1buf);
            ASN1Encodable[] ae = new ASN1Encodable[3];
            ae[0] = _version;
            ae[1] = _oid;
            ae[2] = os;
            DERSequence ds = new DERSequence(ae);
            aos.writeObject(ds);
            aos.flush();
            aos.close();
        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    /**
     * Create aesK and macK from password as in formula above
     * @param password
     * @return
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    private Tuple<SecretKeySpec, SecretKeySpec> initializeForAES(char[] password)
            throws IOException, NoSuchAlgorithmException {
        Tuple<SecretKeySpec, SecretKeySpec> result = null;

        byte[] passwordAsBytes = charToByteArray(password);
        byte[] little = new byte[1];
        SecretKeySpec passK = new SecretKeySpec(passwordAsBytes, _AESKeyAlgorithm);
        try {
            _AESKeyMac.init(passK);
            little[0] = 0;
            byte[] aesKBytes = _AESKeyMac.doFinal(little);
            SecretKeySpec aesK = new SecretKeySpec(aesKBytes, AES_ALGORITHM);
            _macKeyMac.init(passK);
            little[0] = 1;
            byte[] macKBytes = _macKeyMac.doFinal(little);
            SecretKeySpec macK = new SecretKeySpec(macKBytes, MAC_ALGORITHM);
            result = new Tuple<SecretKeySpec, SecretKeySpec>(aesK, macK);
        } catch (Exception e) {
            throw new IOException(e);
        }
        return result;
    }

    private int keyAlgorithmToCipherSize(String algorithm) throws NoSuchAlgorithmException {
        Integer size = _k2Size.get(algorithm);
        if (null == size)
            throw new NoSuchAlgorithmException("Not a recognized algorithm: " + algorithm);
        return size;
    }

    /**
     * Service providers automatically supply the passphrase in a char array but we need
     * a byte array. 
     * 
     * TODO Perhaps this should be moved to DataUtils
     * 
     * @param in
     * @return
     */
    private byte[] charToByteArray(char[] in) {
        byte[] bytes = new byte[in.length];
        for (int i = 0; i < in.length; i++) {
            bytes[i] = (byte) in[i];
        }
        return bytes;
    }
}