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.KeyStoreException;
import java.security.KeyStoreSpi;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
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.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.CryptoServicesRegistrar;
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;
import org.bouncycastle.jcajce.io.CipherInputStream;
import org.bouncycastle.jcajce.io.CipherOutputStream;
import org.bouncycastle.jcajce.util.BCJcaJceHelper;
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.Properties;
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 = CryptoServicesRegistrar.getSecureRandom();

    protected int              version;

    private final JcaJceHelper helper = new BCJcaJceHelper();

    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.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 (enc == null)
        {
            throw new IOException("unable to store encoding of protected key");
        }

        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 BouncyCastleProvider.getPrivateKey(PrivateKeyInfo.getInstance(enc));
            case KEY_PUBLIC:
                return  BouncyCastleProvider.getPublicKey(SubjectPublicKeyInfo.getInstance(enc));
            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))
        {
            if (chain == null)
            {
                throw new KeyStoreException("no certificate chain for private key");
            }
            if (key.getEncoded() == null)
            {
                // we ingore the password as the key is already protected.
                table.put(alias, new StoreEntry(alias, new Date(), KEY, key, chain));
                return;
            }
        }

        try
        {
            table.put(alias, new StoreEntry(alias, key, password, chain));
        }
        catch (Exception e)
        {
            throw new BCKeyStoreException(e.toString(), e);
        }
    }

    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 IOException("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 IOException("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 > (MIN_ITERATIONS << 6)))
            {
                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();
        }
    }

    public static class Std
       extends BcKeyStoreSpi
    {
        public Std()
        {
            super(STORE_VERSION);
        }
    }

    public static class Version1
        extends BcKeyStoreSpi
    {
        public Version1()
        {
            super(1);
            if (!Properties.isOverrideSet("org.bouncycastle.bks.enable_v1"))
            {
                 throw new IllegalStateException("BKS-V1 not enabled");
            }
        }
    }

    private static class BCKeyStoreException
        extends KeyStoreException
    {
        private final Exception cause;

        public BCKeyStoreException(String msg, Exception cause)
        {
            super(msg);
            this.cause = cause;
        }

        public Throwable getCause()
        {
            return cause;
        }
    }
}
