blob: 85365b7d7f4b0d3cdb061400a1b98a5eb267a63a [file] [log] [blame]
package org.bouncycastle.jcajce.provider.keystore.bc;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyStoreException;
import java.security.KeyStoreSpi;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.PBEParametersGenerator;
import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.generators.PKCS12ParametersGenerator;
import org.bouncycastle.crypto.io.DigestInputStream;
import org.bouncycastle.crypto.io.DigestOutputStream;
import org.bouncycastle.crypto.io.MacInputStream;
import org.bouncycastle.crypto.io.MacOutputStream;
import org.bouncycastle.crypto.macs.HMac;
// Android-changed: Use default provider for JCA algorithms instead of BC
// Was: import org.bouncycastle.jcajce.util.BCJcaJceHelper;
import org.bouncycastle.jcajce.util.DefaultJcaJceHelper;
import org.bouncycastle.jcajce.util.JcaJceHelper;
import org.bouncycastle.jce.interfaces.BCKeyStore;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.io.Streams;
import org.bouncycastle.util.io.TeeOutputStream;
public class BcKeyStoreSpi
extends KeyStoreSpi
implements BCKeyStore
{
private static final int STORE_VERSION = 2;
private static final int STORE_SALT_SIZE = 20;
private static final String STORE_CIPHER = "PBEWithSHAAndTwofish-CBC";
private static final int KEY_SALT_SIZE = 20;
private static final int MIN_ITERATIONS = 1024;
private static final String KEY_CIPHER = "PBEWithSHAAnd3-KeyTripleDES-CBC";
//
// generic object types
//
static final int NULL = 0;
static final int CERTIFICATE = 1;
static final int KEY = 2;
static final int SECRET = 3;
static final int SEALED = 4;
//
// key types
//
static final int KEY_PRIVATE = 0;
static final int KEY_PUBLIC = 1;
static final int KEY_SECRET = 2;
protected Hashtable table = new Hashtable();
protected SecureRandom random = new SecureRandom();
protected int version;
// Android-changed: Use default provider for JCA algorithms instead of BC
// Was: private final JcaJceHelper helper = new BCJcaJceHelper();
private final JcaJceHelper helper = new DefaultJcaJceHelper();
public BcKeyStoreSpi(int version)
{
this.version = version;
}
private class StoreEntry
{
int type;
String alias;
Object obj;
Certificate[] certChain;
Date date = new Date();
StoreEntry(
String alias,
Certificate obj)
{
this.type = CERTIFICATE;
this.alias = alias;
this.obj = obj;
this.certChain = null;
}
StoreEntry(
String alias,
byte[] obj,
Certificate[] certChain)
{
this.type = SECRET;
this.alias = alias;
this.obj = obj;
this.certChain = certChain;
}
StoreEntry(
String alias,
Key key,
char[] password,
Certificate[] certChain)
throws Exception
{
this.type = SEALED;
this.alias = alias;
this.certChain = certChain;
byte[] salt = new byte[KEY_SALT_SIZE];
random.setSeed(System.currentTimeMillis());
random.nextBytes(salt);
int iterationCount = MIN_ITERATIONS + (random.nextInt() & 0x3ff);
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
DataOutputStream dOut = new DataOutputStream(bOut);
dOut.writeInt(salt.length);
dOut.write(salt);
dOut.writeInt(iterationCount);
Cipher cipher = makePBECipher(KEY_CIPHER, Cipher.ENCRYPT_MODE, password, salt, iterationCount);
CipherOutputStream cOut = new CipherOutputStream(dOut, cipher);
dOut = new DataOutputStream(cOut);
encodeKey(key, dOut);
dOut.close();
obj = bOut.toByteArray();
}
StoreEntry(
String alias,
Date date,
int type,
Object obj)
{
this.alias = alias;
this.date = date;
this.type = type;
this.obj = obj;
}
StoreEntry(
String alias,
Date date,
int type,
Object obj,
Certificate[] certChain)
{
this.alias = alias;
this.date = date;
this.type = type;
this.obj = obj;
this.certChain = certChain;
}
int getType()
{
return type;
}
String getAlias()
{
return alias;
}
Object getObject()
{
return obj;
}
Object getObject(
char[] password)
throws NoSuchAlgorithmException, UnrecoverableKeyException
{
if (password == null || password.length == 0)
{
if (obj instanceof Key)
{
return obj;
}
}
if (type == SEALED)
{
ByteArrayInputStream bIn = new ByteArrayInputStream((byte[])obj);
DataInputStream dIn = new DataInputStream(bIn);
try
{
byte[] salt = new byte[dIn.readInt()];
dIn.readFully(salt);
int iterationCount = dIn.readInt();
Cipher cipher = makePBECipher(KEY_CIPHER, Cipher.DECRYPT_MODE, password, salt, iterationCount);
CipherInputStream cIn = new CipherInputStream(dIn, cipher);
try
{
return decodeKey(new DataInputStream(cIn));
}
catch (Exception x)
{
bIn = new ByteArrayInputStream((byte[])obj);
dIn = new DataInputStream(bIn);
salt = new byte[dIn.readInt()];
dIn.readFully(salt);
iterationCount = dIn.readInt();
cipher = makePBECipher("Broken" + KEY_CIPHER, Cipher.DECRYPT_MODE, password, salt, iterationCount);
cIn = new CipherInputStream(dIn, cipher);
Key k = null;
try
{
k = decodeKey(new DataInputStream(cIn));
}
catch (Exception y)
{
bIn = new ByteArrayInputStream((byte[])obj);
dIn = new DataInputStream(bIn);
salt = new byte[dIn.readInt()];
dIn.readFully(salt);
iterationCount = dIn.readInt();
cipher = makePBECipher("Old" + KEY_CIPHER, Cipher.DECRYPT_MODE, password, salt, iterationCount);
cIn = new CipherInputStream(dIn, cipher);
k = decodeKey(new DataInputStream(cIn));
}
//
// reencrypt key with correct cipher.
//
if (k != null)
{
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
DataOutputStream dOut = new DataOutputStream(bOut);
dOut.writeInt(salt.length);
dOut.write(salt);
dOut.writeInt(iterationCount);
Cipher out = makePBECipher(KEY_CIPHER, Cipher.ENCRYPT_MODE, password, salt, iterationCount);
CipherOutputStream cOut = new CipherOutputStream(dOut, out);
dOut = new DataOutputStream(cOut);
encodeKey(k, dOut);
dOut.close();
obj = bOut.toByteArray();
return k;
}
else
{
throw new UnrecoverableKeyException("no match");
}
}
}
catch (Exception e)
{
throw new UnrecoverableKeyException("no match");
}
}
else
{
throw new RuntimeException("forget something!");
// TODO
// if we get to here key was saved as byte data, which
// according to the docs means it must be a private key
// in EncryptedPrivateKeyInfo (PKCS8 format), later...
//
}
}
Certificate[] getCertificateChain()
{
return certChain;
}
Date getDate()
{
return date;
}
}
private void encodeCertificate(
Certificate cert,
DataOutputStream dOut)
throws IOException
{
try
{
byte[] cEnc = cert.getEncoded();
dOut.writeUTF(cert.getType());
dOut.writeInt(cEnc.length);
dOut.write(cEnc);
}
catch (CertificateEncodingException ex)
{
throw new IOException(ex.toString());
}
}
private Certificate decodeCertificate(
DataInputStream dIn)
throws IOException
{
String type = dIn.readUTF();
byte[] cEnc = new byte[dIn.readInt()];
dIn.readFully(cEnc);
try
{
CertificateFactory cFact = helper.createCertificateFactory(type);
ByteArrayInputStream bIn = new ByteArrayInputStream(cEnc);
return cFact.generateCertificate(bIn);
}
catch (NoSuchProviderException ex)
{
throw new IOException(ex.toString());
}
catch (CertificateException ex)
{
throw new IOException(ex.toString());
}
}
private void encodeKey(
Key key,
DataOutputStream dOut)
throws IOException
{
byte[] enc = key.getEncoded();
if (key instanceof PrivateKey)
{
dOut.write(KEY_PRIVATE);
}
else if (key instanceof PublicKey)
{
dOut.write(KEY_PUBLIC);
}
else
{
dOut.write(KEY_SECRET);
}
dOut.writeUTF(key.getFormat());
dOut.writeUTF(key.getAlgorithm());
dOut.writeInt(enc.length);
dOut.write(enc);
}
private Key decodeKey(
DataInputStream dIn)
throws IOException
{
int keyType = dIn.read();
String format = dIn.readUTF();
String algorithm = dIn.readUTF();
byte[] enc = new byte[dIn.readInt()];
KeySpec spec;
dIn.readFully(enc);
if (format.equals("PKCS#8") || format.equals("PKCS8"))
{
spec = new PKCS8EncodedKeySpec(enc);
}
else if (format.equals("X.509") || format.equals("X509"))
{
spec = new X509EncodedKeySpec(enc);
}
else if (format.equals("RAW"))
{
return new SecretKeySpec(enc, algorithm);
}
else
{
throw new IOException("Key format " + format + " not recognised!");
}
try
{
switch (keyType)
{
case KEY_PRIVATE:
return helper.createKeyFactory(algorithm).generatePrivate(spec);
case KEY_PUBLIC:
return helper.createKeyFactory(algorithm).generatePublic(spec);
case KEY_SECRET:
return helper.createSecretKeyFactory(algorithm).generateSecret(spec);
default:
throw new IOException("Key type " + keyType + " not recognised!");
}
}
catch (Exception e)
{
throw new IOException("Exception creating key: " + e.toString());
}
}
protected Cipher makePBECipher(
String algorithm,
int mode,
char[] password,
byte[] salt,
int iterationCount)
throws IOException
{
try
{
PBEKeySpec pbeSpec = new PBEKeySpec(password);
SecretKeyFactory keyFact = helper.createSecretKeyFactory(algorithm);
PBEParameterSpec defParams = new PBEParameterSpec(salt, iterationCount);
Cipher cipher = helper.createCipher(algorithm);
cipher.init(mode, keyFact.generateSecret(pbeSpec), defParams);
return cipher;
}
catch (Exception e)
{
throw new IOException("Error initialising store of key store: " + e);
}
}
public void setRandom(
SecureRandom rand)
{
this.random = rand;
}
public Enumeration engineAliases()
{
return table.keys();
}
public boolean engineContainsAlias(
String alias)
{
return (table.get(alias) != null);
}
public void engineDeleteEntry(
String alias)
throws KeyStoreException
{
Object entry = table.get(alias);
if (entry == null)
{
return;
}
table.remove(alias);
}
public Certificate engineGetCertificate(
String alias)
{
StoreEntry entry = (StoreEntry)table.get(alias);
if (entry != null)
{
if (entry.getType() == CERTIFICATE)
{
return (Certificate)entry.getObject();
}
else
{
Certificate[] chain = entry.getCertificateChain();
if (chain != null)
{
return chain[0];
}
}
}
return null;
}
public String engineGetCertificateAlias(
Certificate cert)
{
Enumeration e = table.elements();
while (e.hasMoreElements())
{
StoreEntry entry = (StoreEntry)e.nextElement();
if (entry.getObject() instanceof Certificate)
{
Certificate c = (Certificate)entry.getObject();
if (c.equals(cert))
{
return entry.getAlias();
}
}
else
{
Certificate[] chain = entry.getCertificateChain();
if (chain != null && chain[0].equals(cert))
{
return entry.getAlias();
}
}
}
return null;
}
public Certificate[] engineGetCertificateChain(
String alias)
{
StoreEntry entry = (StoreEntry)table.get(alias);
if (entry != null)
{
return entry.getCertificateChain();
}
return null;
}
public Date engineGetCreationDate(String alias)
{
StoreEntry entry = (StoreEntry)table.get(alias);
if (entry != null)
{
return entry.getDate();
}
return null;
}
public Key engineGetKey(
String alias,
char[] password)
throws NoSuchAlgorithmException, UnrecoverableKeyException
{
StoreEntry entry = (StoreEntry)table.get(alias);
if (entry == null || entry.getType() == CERTIFICATE)
{
return null;
}
return (Key)entry.getObject(password);
}
public boolean engineIsCertificateEntry(
String alias)
{
StoreEntry entry = (StoreEntry)table.get(alias);
if (entry != null && entry.getType() == CERTIFICATE)
{
return true;
}
return false;
}
public boolean engineIsKeyEntry(
String alias)
{
StoreEntry entry = (StoreEntry)table.get(alias);
if (entry != null && entry.getType() != CERTIFICATE)
{
return true;
}
return false;
}
public void engineSetCertificateEntry(
String alias,
Certificate cert)
throws KeyStoreException
{
StoreEntry entry = (StoreEntry)table.get(alias);
if (entry != null && entry.getType() != CERTIFICATE)
{
throw new KeyStoreException("key store already has a key entry with alias " + alias);
}
table.put(alias, new StoreEntry(alias, cert));
}
public void engineSetKeyEntry(
String alias,
byte[] key,
Certificate[] chain)
throws KeyStoreException
{
table.put(alias, new StoreEntry(alias, key, chain));
}
public void engineSetKeyEntry(
String alias,
Key key,
char[] password,
Certificate[] chain)
throws KeyStoreException
{
if ((key instanceof PrivateKey) && (chain == null))
{
throw new KeyStoreException("no certificate chain for private key");
}
try
{
table.put(alias, new StoreEntry(alias, key, password, chain));
}
catch (Exception e)
{
throw new KeyStoreException(e.toString());
}
}
public int engineSize()
{
return table.size();
}
protected void loadStore(
InputStream in)
throws IOException
{
DataInputStream dIn = new DataInputStream(in);
int type = dIn.read();
while (type > NULL)
{
String alias = dIn.readUTF();
Date date = new Date(dIn.readLong());
int chainLength = dIn.readInt();
Certificate[] chain = null;
if (chainLength != 0)
{
chain = new Certificate[chainLength];
for (int i = 0; i != chainLength; i++)
{
chain[i] = decodeCertificate(dIn);
}
}
switch (type)
{
case CERTIFICATE:
Certificate cert = decodeCertificate(dIn);
table.put(alias, new StoreEntry(alias, date, CERTIFICATE, cert));
break;
case KEY:
Key key = decodeKey(dIn);
table.put(alias, new StoreEntry(alias, date, KEY, key, chain));
break;
case SECRET:
case SEALED:
byte[] b = new byte[dIn.readInt()];
dIn.readFully(b);
table.put(alias, new StoreEntry(alias, date, type, b, chain));
break;
default:
throw new RuntimeException("Unknown object type in store.");
}
type = dIn.read();
}
}
protected void saveStore(
OutputStream out)
throws IOException
{
Enumeration e = table.elements();
DataOutputStream dOut = new DataOutputStream(out);
while (e.hasMoreElements())
{
StoreEntry entry = (StoreEntry)e.nextElement();
dOut.write(entry.getType());
dOut.writeUTF(entry.getAlias());
dOut.writeLong(entry.getDate().getTime());
Certificate[] chain = entry.getCertificateChain();
if (chain == null)
{
dOut.writeInt(0);
}
else
{
dOut.writeInt(chain.length);
for (int i = 0; i != chain.length; i++)
{
encodeCertificate(chain[i], dOut);
}
}
switch (entry.getType())
{
case CERTIFICATE:
encodeCertificate((Certificate)entry.getObject(), dOut);
break;
case KEY:
encodeKey((Key)entry.getObject(), dOut);
break;
case SEALED:
case SECRET:
byte[] b = (byte[])entry.getObject();
dOut.writeInt(b.length);
dOut.write(b);
break;
default:
throw new RuntimeException("Unknown object type in store.");
}
}
dOut.write(NULL);
}
public void engineLoad(
InputStream stream,
char[] password)
throws IOException
{
table.clear();
if (stream == null) // just initialising
{
return;
}
DataInputStream dIn = new DataInputStream(stream);
int version = dIn.readInt();
if (version != STORE_VERSION)
{
if (version != 0 && version != 1)
{
throw new IOException("Wrong version of key store.");
}
}
int saltLength = dIn.readInt();
if (saltLength <= 0)
{
throw new IOException("Invalid salt detected");
}
byte[] salt = new byte[saltLength];
dIn.readFully(salt);
int iterationCount = dIn.readInt();
//
// we only do an integrity check if the password is provided.
//
HMac hMac = new HMac(new SHA1Digest());
if (password != null && password.length != 0)
{
byte[] passKey = PBEParametersGenerator.PKCS12PasswordToBytes(password);
PBEParametersGenerator pbeGen = new PKCS12ParametersGenerator(new SHA1Digest());
pbeGen.init(passKey, salt, iterationCount);
CipherParameters macParams;
if (version != 2)
{
macParams = pbeGen.generateDerivedMacParameters(hMac.getMacSize());
}
else
{
macParams = pbeGen.generateDerivedMacParameters(hMac.getMacSize() * 8);
}
Arrays.fill(passKey, (byte)0);
hMac.init(macParams);
MacInputStream mIn = new MacInputStream(dIn, hMac);
loadStore(mIn);
// Finalise our mac calculation
byte[] mac = new byte[hMac.getMacSize()];
hMac.doFinal(mac, 0);
// TODO Should this actually be reading the remainder of the stream?
// Read the original mac from the stream
byte[] oldMac = new byte[hMac.getMacSize()];
dIn.readFully(oldMac);
if (!Arrays.constantTimeAreEqual(mac, oldMac))
{
table.clear();
throw new IOException("KeyStore integrity check failed.");
}
}
else
{
loadStore(dIn);
// TODO Should this actually be reading the remainder of the stream?
// Parse the original mac from the stream too
byte[] oldMac = new byte[hMac.getMacSize()];
dIn.readFully(oldMac);
}
}
public void engineStore(OutputStream stream, char[] password)
throws IOException
{
DataOutputStream dOut = new DataOutputStream(stream);
byte[] salt = new byte[STORE_SALT_SIZE];
int iterationCount = MIN_ITERATIONS + (random.nextInt() & 0x3ff);
random.nextBytes(salt);
dOut.writeInt(version);
dOut.writeInt(salt.length);
dOut.write(salt);
dOut.writeInt(iterationCount);
HMac hMac = new HMac(new SHA1Digest());
MacOutputStream mOut = new MacOutputStream(hMac);
PBEParametersGenerator pbeGen = new PKCS12ParametersGenerator(new SHA1Digest());
byte[] passKey = PBEParametersGenerator.PKCS12PasswordToBytes(password);
pbeGen.init(passKey, salt, iterationCount);
if (version < 2)
{
hMac.init(pbeGen.generateDerivedMacParameters(hMac.getMacSize()));
}
else
{
hMac.init(pbeGen.generateDerivedMacParameters(hMac.getMacSize() * 8));
}
for (int i = 0; i != passKey.length; i++)
{
passKey[i] = 0;
}
saveStore(new TeeOutputStream(dOut, mOut));
byte[] mac = new byte[hMac.getMacSize()];
hMac.doFinal(mac, 0);
dOut.write(mac);
dOut.close();
}
/**
* the BouncyCastle store. This wont work with the key tool as the
* store is stored encrypted on disk, so the password is mandatory,
* however if you hard drive is in a bad part of town and you absolutely,
* positively, don't want nobody peeking at your things, this is the
* one to use, no problem! After all in a Bouncy Castle nothing can
* touch you.
*
* Also referred to by the alias UBER.
*/
public static class BouncyCastleStore
extends BcKeyStoreSpi
{
public BouncyCastleStore()
{
super(1);
}
public void engineLoad(
InputStream stream,
char[] password)
throws IOException
{
table.clear();
if (stream == null) // just initialising
{
return;
}
DataInputStream dIn = new DataInputStream(stream);
int version = dIn.readInt();
if (version != STORE_VERSION)
{
if (version != 0 && version != 1)
{
throw new IOException("Wrong version of key store.");
}
}
byte[] salt = new byte[dIn.readInt()];
if (salt.length != STORE_SALT_SIZE)
{
throw new IOException("Key store corrupted.");
}
dIn.readFully(salt);
int iterationCount = dIn.readInt();
if ((iterationCount < 0) || (iterationCount > 4 * MIN_ITERATIONS))
{
throw new IOException("Key store corrupted.");
}
String cipherAlg;
if (version == 0)
{
cipherAlg = "Old" + STORE_CIPHER;
}
else
{
cipherAlg = STORE_CIPHER;
}
Cipher cipher = this.makePBECipher(cipherAlg, Cipher.DECRYPT_MODE, password, salt, iterationCount);
CipherInputStream cIn = new CipherInputStream(dIn, cipher);
Digest dig = new SHA1Digest();
DigestInputStream dgIn = new DigestInputStream(cIn, dig);
this.loadStore(dgIn);
// Finalise our digest calculation
byte[] hash = new byte[dig.getDigestSize()];
dig.doFinal(hash, 0);
// TODO Should this actually be reading the remainder of the stream?
// Read the original digest from the stream
byte[] oldHash = new byte[dig.getDigestSize()];
Streams.readFully(cIn, oldHash);
if (!Arrays.constantTimeAreEqual(hash, oldHash))
{
table.clear();
throw new IOException("KeyStore integrity check failed.");
}
}
public void engineStore(OutputStream stream, char[] password)
throws IOException
{
Cipher cipher;
DataOutputStream dOut = new DataOutputStream(stream);
byte[] salt = new byte[STORE_SALT_SIZE];
int iterationCount = MIN_ITERATIONS + (random.nextInt() & 0x3ff);
random.nextBytes(salt);
dOut.writeInt(version);
dOut.writeInt(salt.length);
dOut.write(salt);
dOut.writeInt(iterationCount);
cipher = this.makePBECipher(STORE_CIPHER, Cipher.ENCRYPT_MODE, password, salt, iterationCount);
CipherOutputStream cOut = new CipherOutputStream(dOut, cipher);
DigestOutputStream dgOut = new DigestOutputStream(new SHA1Digest());
this.saveStore(new TeeOutputStream(cOut, dgOut));
byte[] dig = dgOut.getDigest();
cOut.write(dig);
cOut.close();
}
}
static Provider getBouncyCastleProvider()
{
if (Security.getProvider("BC") != null)
{
return Security.getProvider("BC");
}
else
{
return new BouncyCastleProvider();
}
}
public static class Std
extends BcKeyStoreSpi
{
public Std()
{
super(STORE_VERSION);
}
}
public static class Version1
extends BcKeyStoreSpi
{
public Version1()
{
super(1);
}
}
}