/*
* 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;
}
}