package org.bouncycastle.jce.provider;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1Set;
import org.bouncycastle.asn1.BERConstructedOctetString;
import org.bouncycastle.asn1.BEROutputStream;
import org.bouncycastle.asn1.DERBMPString;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.DERObject;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.pkcs.AuthenticatedSafe;
import org.bouncycastle.asn1.pkcs.CertBag;
import org.bouncycastle.asn1.pkcs.ContentInfo;
import org.bouncycastle.asn1.pkcs.EncryptedData;
import org.bouncycastle.asn1.pkcs.MacData;
import org.bouncycastle.asn1.pkcs.PKCS12PBEParams;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.Pfx;
import org.bouncycastle.asn1.pkcs.SafeBag;
import org.bouncycastle.asn1.util.ASN1Dump;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.DigestInfo;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
import org.bouncycastle.jce.interfaces.BCKeyStore;
import org.bouncycastle.jce.interfaces.PKCS12BagAttributeCarrier;
import org.bouncycastle.util.Strings;
import org.bouncycastle.util.encoders.Hex;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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.Principal;
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.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

public class JDKPKCS12KeyStore
    extends KeyStoreSpi
    implements PKCSObjectIdentifiers, X509ObjectIdentifiers, BCKeyStore
{
    private static final int    SALT_SIZE = 20;
    private static final int    MIN_ITERATIONS = 1024;
    
    //
    // SHA-1 and 3-key-triple DES.
    //
    private static final String KEY_ALGORITHM = "1.2.840.113549.1.12.1.3";

    //
    // SHA-1 and 40 bit RC2.
    //
    private static final String CERT_ALGORITHM = "1.2.840.113549.1.12.1.6";

    private IgnoresCaseHashtable            keys = new IgnoresCaseHashtable();
    private Hashtable                       localIds = new Hashtable();
    private IgnoresCaseHashtable            certs = new IgnoresCaseHashtable();
    private Hashtable                       chainCerts = new Hashtable();
    private Hashtable                       keyCerts = new Hashtable();

    //
    // 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 SecureRandom      random = new SecureRandom();

    private CertificateFactory  certFact = null;

    private class CertId
    {
        byte[]  id;

        CertId(
            PublicKey  key)
        {
            this.id = createSubjectKeyId(key).getKeyIdentifier();
        }

        CertId(
            byte[]  id)
        {
            this.id = id;
        }

        public int hashCode()
        {
            int hash = id[0] & 0xff;

            for (int i = 1; i != id.length - 4; i++)
            {
                hash ^= ((id[i] & 0xff) << 24) | ((id[i + 1] & 0xff) << 16)
                          | ((id[i + 2] & 0xff) << 8) | (id[i + 3] & 0xff);
            }

            return hash;
        }

        public boolean equals(
            Object  o)
        {
            if (!(o instanceof CertId))
            {
                return false;
            }

            CertId  cId = (CertId)o;

            if (cId.id.length != id.length)
            {
                return false;
            }

            for (int i = 0; i != id.length; i++)
            {
                if (cId.id[i] != id[i])
                {
                    return false;
                }
            }

            return true;
        }
    }

    public JDKPKCS12KeyStore(
        String provider)
    {
        try
        {
            if (provider != null)
            {
                certFact = CertificateFactory.getInstance("X.509", provider);
            }
            else
            {
                certFact = CertificateFactory.getInstance("X.509");
            }
        }
        catch (Exception e)
        {
            throw new IllegalArgumentException("can't create cert factory - " + e.toString());
        }
    }

    private SubjectKeyIdentifier createSubjectKeyId(
        PublicKey   pubKey)
    {
        try
        {
            SubjectPublicKeyInfo info = new SubjectPublicKeyInfo(
                (ASN1Sequence)new ASN1InputStream(pubKey.getEncoded()).readObject());

            return new SubjectKeyIdentifier(info);
        }
        catch (Exception e)
        {
            throw new RuntimeException("error creating key");
        }
    }

    public void setRandom(
        SecureRandom    rand)
    {
        this.random = rand;
    }

    public Enumeration engineAliases() 
    {
        Hashtable  tab = new Hashtable();

        Enumeration e = certs.keys();
        while (e.hasMoreElements())
        {
            tab.put(e.nextElement(), "cert");
        }

        e = keys.keys();
        while (e.hasMoreElements())
        {
            String  a = (String)e.nextElement();
            if (tab.get(a) == null)
            {
                tab.put(a, "key");
            }
        }

        return tab.keys();
    }

    public boolean engineContainsAlias(
        String  alias) 
    {
        return (certs.get(alias) != null || keys.get(alias) != null);
    }

    /**
     * this is quite complete - we should follow up on the chain, a bit
     * tricky if a certificate appears in more than one chain...
     */
    public void engineDeleteEntry(
        String  alias) 
        throws KeyStoreException
    {
        Key k = (Key)keys.remove(alias);

        Certificate c = (Certificate)certs.remove(alias);

        if (c != null)
        {
            chainCerts.remove(new CertId(c.getPublicKey()));
        }

        if (k != null)
        {
            String  id = (String)localIds.remove(alias);
            if (id != null)
            {
                c = (Certificate)keyCerts.remove(id);
            }
            if (c != null)
            {
                chainCerts.remove(new CertId(c.getPublicKey()));
            }
        }

        if (c == null && k == null)
        {
            throw new KeyStoreException("no such entry as " + alias);
        }
    }

    /**
     * simply return the cert for the private key
     */
    public Certificate engineGetCertificate(
        String alias) 
    {
        if (alias == null)
        {
            throw new IllegalArgumentException("null alias passed to getCertificate.");
        }
        
        Certificate c = (Certificate)certs.get(alias);

        //
        // look up the key table - and try the local key id
        //
        if (c == null)
        {
            String  id = (String)localIds.get(alias);
            if (id != null)
            {
                c = (Certificate)keyCerts.get(id);
            }
            else
            {
                c = (Certificate)keyCerts.get(alias);
            }
        }

        return c;
    }

    public String engineGetCertificateAlias(
        Certificate cert) 
    {
        Enumeration c = certs.elements();
        Enumeration k = certs.keys();

        while (c.hasMoreElements())
        {
            Certificate tc = (Certificate)c.nextElement();
            String      ta = (String)k.nextElement();

            if (tc.equals(cert))
            {
                return ta;
            }
        }

        c = keyCerts.elements();
        k = keyCerts.keys();

        while (c.hasMoreElements())
        {
            Certificate tc = (Certificate)c.nextElement();
            String      ta = (String)k.nextElement();

            if (tc.equals(cert))
            {
                return ta;
            }
        }
        
        return null;
    }
    
    public Certificate[] engineGetCertificateChain(
        String alias) 
    {
        if (alias == null)
        {
            throw new IllegalArgumentException("null alias passed to getCertificateChain.");
        }
        
        if (!engineIsKeyEntry(alias))
        {
            return null;
        }
        
        Certificate c = engineGetCertificate(alias);

        if (c != null)
        {
            Vector  cs = new Vector();

            while (c != null)
            {
                X509Certificate     x509c = (X509Certificate)c;
                Certificate         nextC = null;

                byte[]  bytes = x509c.getExtensionValue(X509Extensions.AuthorityKeyIdentifier.getId());
                if (bytes != null)
                {
                    try
                    {
                        ASN1InputStream         aIn = new ASN1InputStream(bytes);

                        byte[] authBytes = ((ASN1OctetString)aIn.readObject()).getOctets();
                        aIn = new ASN1InputStream(authBytes);

                        AuthorityKeyIdentifier id = new AuthorityKeyIdentifier((ASN1Sequence)aIn.readObject());
                        if (id.getKeyIdentifier() != null)
                        {
                            nextC = (Certificate)chainCerts.get(new CertId(id.getKeyIdentifier()));
                        }
                        
                    }
                    catch (IOException e)
                    {
                        throw new RuntimeException(e.toString());
                    }
                }

                if (nextC == null)
                {
                    //
                    // no authority key id, try the Issuer DN
                    //
                    Principal  i = x509c.getIssuerDN();
                    Principal  s = x509c.getSubjectDN();

                    if (!i.equals(s))
                    {
                        Enumeration e = chainCerts.keys();

                        while (e.hasMoreElements())
                        {
                            X509Certificate crt = (X509Certificate)chainCerts.get(e.nextElement());
                            Principal  sub = crt.getSubjectDN();
                            if (sub.equals(i))
                            {
                                try
                                {
                                    x509c.verify(crt.getPublicKey());
                                    nextC = crt;
                                    break;
                                }
                                catch (Exception ex)
                                {
                                    // continue
                                }
                            }
                        }
                    }
                }

                cs.addElement(c);
                if (nextC != c)     // self signed - end of the chain
                {
                    c = nextC;
                }
                else
                {
                    c = null;
                }
            }

            Certificate[]   certChain = new Certificate[cs.size()];

            for (int i = 0; i != certChain.length; i++)
            {
                certChain[i] = (Certificate)cs.elementAt(i);
            }

            return certChain;
        }

        return null;
    }
    
    public Date engineGetCreationDate(String alias) 
    {
        return new Date();
    }

    public Key engineGetKey(
        String alias,
        char[] password) 
        throws NoSuchAlgorithmException, UnrecoverableKeyException
    {
        if (alias == null)
        {
            throw new IllegalArgumentException("null alias passed to getKey.");
        }
        
        return (Key)keys.get(alias);
    }

    public boolean engineIsCertificateEntry(
        String alias) 
    {
        return (certs.get(alias) != null && keys.get(alias) == null);
    }

    public boolean engineIsKeyEntry(
        String alias) 
    {
        return (keys.get(alias) != null);
    }

    public void engineSetCertificateEntry(
        String      alias,
        Certificate cert) 
        throws KeyStoreException
    {
        if (certs.get(alias) != null)
        {
            throw new KeyStoreException("There is already a certificate with the name " + alias + ".");
        }

        certs.put(alias, cert);
        chainCerts.put(new CertId(cert.getPublicKey()), cert);
    }

    public void engineSetKeyEntry(
        String alias,
        byte[] key,
        Certificate[] chain) 
        throws KeyStoreException
    {
        throw new RuntimeException("operation not supported");
    }

    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");
        }

        if (keys.get(alias) != null && !key.equals(keys.get(alias)))
        {
            throw new KeyStoreException("There is already a key with the name " + alias + ".");
        }

        keys.put(alias, key);
        certs.put(alias, chain[0]);

        for (int i = 0; i != chain.length; i++)
        {
            chainCerts.put(new CertId(chain[i].getPublicKey()), chain[i]);
        }
    }

    public int engineSize() 
    {
        Hashtable  tab = new Hashtable();

        Enumeration e = certs.keys();
        while (e.hasMoreElements())
        {
            tab.put(e.nextElement(), "cert");
        }

        e = keys.keys();
        while (e.hasMoreElements())
        {
            String  a = (String)e.nextElement();
            if (tab.get(a) == null)
            {
                tab.put(a, "key");
            }
        }

        return tab.size();
    }

    protected PrivateKey unwrapKey(
        AlgorithmIdentifier   algId,
        byte[]                data,
        char[]                password,
        boolean               wrongPKCS12Zero)
        throws IOException
    {
        String              algorithm = algId.getObjectId().getId();
        PKCS12PBEParams     pbeParams = new PKCS12PBEParams((ASN1Sequence)algId.getParameters());

        PBEKeySpec          pbeSpec = new PBEKeySpec(password);
        PrivateKey          out;

        try
        {
            SecretKeyFactory    keyFact = SecretKeyFactory.getInstance(
                                                algorithm, "BC");
            PBEParameterSpec    defParams = new PBEParameterSpec(
                                                pbeParams.getIV(),
                                                pbeParams.getIterations().intValue());

            SecretKey           k = keyFact.generateSecret(pbeSpec);
            
            ((JCEPBEKey)k).setTryWrongPKCS12Zero(wrongPKCS12Zero);

            Cipher cipher = Cipher.getInstance(algorithm, "BC");

            cipher.init(Cipher.UNWRAP_MODE, k, defParams);

            // we pass "" as the key algorithm type as it is unknown at this point
            out = (PrivateKey)cipher.unwrap(data, "", Cipher.PRIVATE_KEY);
        }
        catch (Exception e)
        {
            throw new IOException("exception unwrapping private key - " + e.toString());
        }

        return out;
    }

    protected byte[] wrapKey(
        String                  algorithm,
        Key                     key,
        PKCS12PBEParams         pbeParams,
        char[]                  password)
        throws IOException
    {
        PBEKeySpec          pbeSpec = new PBEKeySpec(password);
        byte[]              out;

        try
        {
            SecretKeyFactory    keyFact = SecretKeyFactory.getInstance(
                                                algorithm, "BC");
            PBEParameterSpec    defParams = new PBEParameterSpec(
                                                pbeParams.getIV(),
                                                pbeParams.getIterations().intValue());

            Cipher cipher = Cipher.getInstance(algorithm, "BC");

            cipher.init(Cipher.WRAP_MODE, keyFact.generateSecret(pbeSpec), defParams);

            out = cipher.wrap(key);
        }
        catch (Exception e)
        {
            throw new IOException("exception encrypting data - " + e.toString());
        }

        return out;
    }

    protected ASN1Sequence decryptData(
        AlgorithmIdentifier   algId,
        byte[]                data,
        char[]                password,
        boolean               wrongPKCS12Zero)
        throws IOException
    {
        String              algorithm = algId.getObjectId().getId();
        PKCS12PBEParams     pbeParams = new PKCS12PBEParams((ASN1Sequence)algId.getParameters());

        PBEKeySpec          pbeSpec = new PBEKeySpec(password);
        byte[]              out;

        try
        {
            SecretKeyFactory    keyFact = SecretKeyFactory.getInstance(
                                                algorithm, "BC");
            PBEParameterSpec    defParams = new PBEParameterSpec(
                                                pbeParams.getIV(),
                                                pbeParams.getIterations().intValue());
            SecretKey           k = keyFact.generateSecret(pbeSpec);
            
            ((JCEPBEKey)k).setTryWrongPKCS12Zero(wrongPKCS12Zero);

            Cipher cipher = Cipher.getInstance(algorithm, "BC");

            cipher.init(Cipher.DECRYPT_MODE, k, defParams);

            out = cipher.doFinal(data);
        }
        catch (Exception e)
        {
            throw new IOException("exception decrypting data - " + e.toString());
        }

        ASN1InputStream  aIn = new ASN1InputStream(out);

        return (ASN1Sequence)aIn.readObject();
    }

    protected byte[] encryptData(
        String                  algorithm,
        byte[]                  data,
        PKCS12PBEParams         pbeParams,
        char[]                  password)
        throws IOException
    {
        PBEKeySpec          pbeSpec = new PBEKeySpec(password);
        byte[]              out;

        try
        {
            SecretKeyFactory    keyFact = SecretKeyFactory.getInstance(
                                                algorithm, "BC");
            PBEParameterSpec    defParams = new PBEParameterSpec(
                                                pbeParams.getIV(),
                                                pbeParams.getIterations().intValue());

            Cipher cipher = Cipher.getInstance(algorithm, "BC");

            cipher.init(Cipher.ENCRYPT_MODE, keyFact.generateSecret(pbeSpec), defParams);

            out = cipher.doFinal(data);
        }
        catch (Exception e)
        {
            throw new IOException("exception encrypting data - " + e.toString());
        }

        return out;
    }

    public void engineLoad(
        InputStream stream,
        char[]      password) 
        throws IOException
    {
        if (stream == null)     // just initialising
        {
            return;
        }

        if (password == null)
        {
            throw new NullPointerException("No password supplied for PKCS#12 KeyStore.");
        }

        BufferedInputStream             bufIn = new BufferedInputStream(stream);

        bufIn.mark(10);

        int head = bufIn.read();

        if (head != 0x30)
        {
            throw new IOException("stream does not represent a PKCS12 key store");
        }

        bufIn.reset();

        ASN1InputStream bIn = new ASN1InputStream(bufIn);
        ASN1Sequence    obj = (ASN1Sequence)bIn.readObject();
        Pfx             bag = new Pfx(obj);
        ContentInfo     info = bag.getAuthSafe();
        Vector          chain = new Vector();
        boolean         unmarkedKey = false;
        boolean         wrongPKCS12Zero = false;

        if (bag.getMacData() != null)           // check the mac code
        {
            ByteArrayOutputStream       bOut = new ByteArrayOutputStream();
            BEROutputStream             berOut = new BEROutputStream(bOut);
            MacData                     mData = bag.getMacData();
            DigestInfo                  dInfo = mData.getMac();
            AlgorithmIdentifier         algId = dInfo.getAlgorithmId();
            byte[]                      salt = mData.getSalt();
            int                         itCount = mData.getIterationCount().intValue();
        
            berOut.writeObject(info);

            byte[]  data = ((ASN1OctetString)info.getContent()).getOctets();

            try
            {
                Mac                 mac = Mac.getInstance(algId.getObjectId().getId(), "BC");
                SecretKeyFactory    keyFact = SecretKeyFactory.getInstance(algId.getObjectId().getId(), "BC");
                PBEParameterSpec    defParams = new PBEParameterSpec(salt, itCount);
                PBEKeySpec          pbeSpec = new PBEKeySpec(password);

                mac.init(keyFact.generateSecret(pbeSpec), defParams);

                mac.update(data);

                byte[]  res = mac.doFinal();
                byte[]  dig = dInfo.getDigest();

                if (res.length != dInfo.getDigest().length)
                {
                    throw new IOException("PKCS12 key store mac invalid - wrong password or corrupted file.");
                }

                boolean okay = true;
                
                for (int i = 0; i != res.length; i++)
                {
                    if (res[i] != dig[i])
                    {
                        if (password.length != 0)  // may be dodgey zero password
                        {
                            throw new IOException("PKCS12 key store mac invalid - wrong password or corrupted file.");
                        }
                        else
                        {
                            okay = false;
                            break;
                        }
                    }
                }
                
                //
                // may be incorrect zero length password
                //
                if (!okay)
                {
                    SecretKey k = keyFact.generateSecret(pbeSpec);
                    
                    ((JCEPBEKey)k).setTryWrongPKCS12Zero(true);
                    
                    mac.init(k, defParams);
    
                    mac.update(data);
    
                    res = mac.doFinal();
                    dig = dInfo.getDigest();
                    
                    for (int i = 0; i != res.length; i++)
                    {
                        if (res[i] != dig[i])
                        {
                           throw new IOException("PKCS12 key store mac invalid - wrong password or corrupted file.");
                        }
                    }
                    
                    wrongPKCS12Zero = true;
                }
            }
            catch (IOException e)
            {
                throw e;
            }
            catch (Exception e)
            {
                throw new IOException("error constructing MAC: " + e.toString());
            }
        }

        keys = new IgnoresCaseHashtable();
        localIds = new Hashtable();

        if (info.getContentType().equals(data))
        {
            bIn = new ASN1InputStream(((ASN1OctetString)info.getContent()).getOctets());

            AuthenticatedSafe   authSafe = new AuthenticatedSafe((ASN1Sequence)bIn.readObject());
            ContentInfo[]       c = authSafe.getContentInfo();

            for (int i = 0; i != c.length; i++)
            {
                if (c[i].getContentType().equals(data))
                {
                    ASN1InputStream dIn = new ASN1InputStream(((ASN1OctetString)c[i].getContent()).getOctets());
                    ASN1Sequence    seq = (ASN1Sequence)dIn.readObject();

                    for (int j = 0; j != seq.size(); j++)
                    {
                        SafeBag b = new SafeBag((ASN1Sequence)seq.getObjectAt(j));
                        if (b.getBagId().equals(pkcs8ShroudedKeyBag))
                        {
                            org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo eIn = new org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo((ASN1Sequence)b.getBagValue());
                            PrivateKey              privKey = unwrapKey(eIn.getEncryptionAlgorithm(), eIn.getEncryptedData(), password, wrongPKCS12Zero);

                            //
                            // set the attributes on the key
                            //
                            PKCS12BagAttributeCarrier   bagAttr = (PKCS12BagAttributeCarrier)privKey;
                            String                                   alias = null;
                            ASN1OctetString                   localId = null;

                            if (b.getBagAttributes() != null)
                            {
                                Enumeration e = b.getBagAttributes().getObjects();
                                while (e.hasMoreElements())
                                {
                                    ASN1Sequence  sq = (ASN1Sequence)e.nextElement();
                                    DERObjectIdentifier     aOid = (DERObjectIdentifier)sq.getObjectAt(0);
                                    ASN1Set                 attrSet = (ASN1Set)sq.getObjectAt(1);
                                    DERObject               attr = null;
    
                                    if (attrSet.size() > 0)
                                    {
                                        attr = (DERObject)attrSet.getObjectAt(0);
    
                                        bagAttr.setBagAttribute(aOid, attr);
                                    }
    
                                    if (aOid.equals(pkcs_9_at_friendlyName))
                                    {
                                        alias = ((DERBMPString)attr).getString();
                                        keys.put(alias, privKey);
                                    }
                                    else if (aOid.equals(pkcs_9_at_localKeyId))
                                    {
                                        localId = (ASN1OctetString)attr;
                                    }
                                }
                            }
                        
                            if (localId != null)
                            {
                                String name = new String(Hex.encode(localId.getOctets()));
    
                                if (alias == null)
                                {
                                    keys.put(name, privKey);
                                }
                                else
                                {
                                    localIds.put(alias, name);
                                }
                             }
                             else
                             {
                                 unmarkedKey = true;
                                 keys.put("unmarked", privKey);
                             }
                        }
                        else if (b.getBagId().equals(certBag))
                        {
                            chain.addElement(b);
                        }
                        else
                        {
                            System.out.println("extra in data " + b.getBagId());
                            System.out.println(ASN1Dump.dumpAsString(b));
                        }
                    }
                }
                else if (c[i].getContentType().equals(encryptedData))
                {
                    EncryptedData d = new EncryptedData((ASN1Sequence)c[i].getContent());
                    ASN1Sequence seq = decryptData(d.getEncryptionAlgorithm(), d.getContent().getOctets(), password, wrongPKCS12Zero);

                    for (int j = 0; j != seq.size(); j++)
                    {
                        SafeBag b = new SafeBag((ASN1Sequence)seq.getObjectAt(j));
                        
                        if (b.getBagId().equals(certBag))
                        {
                            chain.addElement(b);
                        }
                        else if (b.getBagId().equals(pkcs8ShroudedKeyBag))
                        {
                            org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo eIn = new org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo((ASN1Sequence)b.getBagValue());
                            PrivateKey              privKey = unwrapKey(eIn.getEncryptionAlgorithm(), eIn.getEncryptedData(), password, wrongPKCS12Zero);

                            //
                            // set the attributes on the key
                            //
                            PKCS12BagAttributeCarrier   bagAttr = (PKCS12BagAttributeCarrier)privKey;
                            String                      alias = null;
                            ASN1OctetString              localId = null;

                            Enumeration e = b.getBagAttributes().getObjects();
                            while (e.hasMoreElements())
                            {
                                ASN1Sequence  sq = (ASN1Sequence)e.nextElement();
                                DERObjectIdentifier     aOid = (DERObjectIdentifier)sq.getObjectAt(0);
                                ASN1Set                 attrSet= (ASN1Set)sq.getObjectAt(1);
                                DERObject               attr = null;

                                if (attrSet.size() > 0)
                                {
                                    attr = (DERObject)attrSet.getObjectAt(0);

                                    bagAttr.setBagAttribute(aOid, attr);
                                }

                                if (aOid.equals(pkcs_9_at_friendlyName))
                                {
                                    alias = ((DERBMPString)attr).getString();
                                    keys.put(alias, privKey);
                                }
                                else if (aOid.equals(pkcs_9_at_localKeyId))
                                {
                                    localId = (ASN1OctetString)attr;
                                }
                            }

                            String name = new String(Hex.encode(localId.getOctets()));

                            if (alias == null)
                            {
                                keys.put(name, privKey);
                            }
                            else
                            {
                                localIds.put(alias, name);
                            }
                        }
                        else if (b.getBagId().equals(keyBag))
                        {
                            org.bouncycastle.asn1.pkcs.PrivateKeyInfo pIn = new org.bouncycastle.asn1.pkcs.PrivateKeyInfo((ASN1Sequence)b.getBagValue());
                            PrivateKey              privKey = JDKKeyFactory.createPrivateKeyFromPrivateKeyInfo(pIn);

                            //
                            // set the attributes on the key
                            //
                            PKCS12BagAttributeCarrier   bagAttr = (PKCS12BagAttributeCarrier)privKey;
                            String                      alias = null;
                            ASN1OctetString             localId = null;

                            Enumeration e = b.getBagAttributes().getObjects();
                            while (e.hasMoreElements())
                            {
                                ASN1Sequence  sq = (ASN1Sequence)e.nextElement();
                                DERObjectIdentifier     aOid = (DERObjectIdentifier)sq.getObjectAt(0);
                                ASN1Set                 attrSet = (ASN1Set)sq.getObjectAt(1);
                                DERObject   attr = null;

                                if (attrSet.size() > 0)
                                {
                                    attr = (DERObject)attrSet.getObjectAt(0);

                                    bagAttr.setBagAttribute(aOid, attr);
                                }

                                if (aOid.equals(pkcs_9_at_friendlyName))
                                {
                                    alias = ((DERBMPString)attr).getString();
                                    keys.put(alias, privKey);
                                }
                                else if (aOid.equals(pkcs_9_at_localKeyId))
                                {
                                    localId = (ASN1OctetString)attr;
                                }
                            }

                            String name = new String(Hex.encode(localId.getOctets()));

                            if (alias == null)
                            {
                                keys.put(name, privKey);
                            }
                            else
                            {
                                localIds.put(alias, name);
                            }
                        }
                        else
                        {
                            System.out.println("extra in encryptedData " + b.getBagId());
                            System.out.println(ASN1Dump.dumpAsString(b));
                        }
                    }
                }
                else
                {
                    System.out.println("extra " + c[i].getContentType().getId());
                    System.out.println("extra " + ASN1Dump.dumpAsString(c[i].getContent()));
                }
            }
        }

        certs = new IgnoresCaseHashtable();
        chainCerts = new Hashtable();
        keyCerts = new Hashtable();

        for (int i = 0; i != chain.size(); i++)
        {
            SafeBag     b = (SafeBag)chain.elementAt(i);
            CertBag     cb = new CertBag((ASN1Sequence)b.getBagValue());
            Certificate cert;

            try
            {
                ByteArrayInputStream  cIn = new ByteArrayInputStream(
                                ((ASN1OctetString)cb.getCertValue()).getOctets());
                cert = certFact.generateCertificate(cIn);
            }
            catch (Exception e)
            {
                throw new RuntimeException(e.toString());
            }


            //
            // set the attributes
            //
            ASN1OctetString              localId = null;
            String                      alias = null;

            if (b.getBagAttributes() != null)
            {
                Enumeration e = b.getBagAttributes().getObjects();
                while (e.hasMoreElements())
                {
                    ASN1Sequence  sq = (ASN1Sequence)e.nextElement();
                    DERObjectIdentifier     oid = (DERObjectIdentifier)sq.getObjectAt(0);
                    DERObject               attr = (DERObject)((ASN1Set)sq.getObjectAt(1)).getObjectAt(0);

                    if (cert instanceof PKCS12BagAttributeCarrier)
                    {
                        PKCS12BagAttributeCarrier   bagAttr = (PKCS12BagAttributeCarrier)cert;
                        bagAttr.setBagAttribute(oid, attr);
                    }

                    if (oid.equals(pkcs_9_at_friendlyName))
                    {
                        alias = ((DERBMPString)attr).getString();
                    }
                    else if (oid.equals(pkcs_9_at_localKeyId))
                    {
                        localId = (ASN1OctetString)attr;
                    }
                }
            }

            chainCerts.put(new CertId(cert.getPublicKey()), cert);

            if (unmarkedKey)
            {
                if (keyCerts.isEmpty())
                {
                    String    name = new String(Hex.encode(createSubjectKeyId(cert.getPublicKey()).getKeyIdentifier()));
                    
                    keyCerts.put(name, cert);
                    keys.put(name, keys.remove("unmarked"));
                }
            }
            else
            {
                //
                // the local key id needs to override the friendly name
                //
                if (localId != null)
                {
                    String name = new String(Hex.encode(localId.getOctets()));

                    keyCerts.put(name, cert);
                }
                if (alias != null)
                {
                    certs.put(alias, cert);
                }
            }
        }
    }

    public void engineStore(OutputStream stream, char[] password) 
        throws IOException
    {
        if (password == null)
        {
            throw new NullPointerException("No password supplied for PKCS#12 KeyStore.");
        }

        ContentInfo[]   c = new ContentInfo[2];


        //
        // handle the key
        //
        ASN1EncodableVector  keyS = new ASN1EncodableVector();


        Enumeration ks = keys.keys();

        while (ks.hasMoreElements())
        {
            byte[]                  kSalt = new byte[SALT_SIZE];

            random.nextBytes(kSalt);

            String                  name = (String)ks.nextElement();
            PrivateKey              privKey = (PrivateKey)keys.get(name);
            PKCS12PBEParams         kParams = new PKCS12PBEParams(kSalt, MIN_ITERATIONS);
            byte[]                  kBytes = wrapKey(KEY_ALGORITHM, privKey, kParams, password);
            AlgorithmIdentifier     kAlgId = new AlgorithmIdentifier(new DERObjectIdentifier(KEY_ALGORITHM), kParams.getDERObject());
            org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo kInfo = new org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo(kAlgId, kBytes);
            boolean                 attrSet = false;
            ASN1EncodableVector     kName = new ASN1EncodableVector();

            if (privKey instanceof PKCS12BagAttributeCarrier)
            {
                PKCS12BagAttributeCarrier   bagAttrs = (PKCS12BagAttributeCarrier)privKey;
                //
                // make sure we are using the local alias on store
                //
                DERBMPString    nm = (DERBMPString)bagAttrs.getBagAttribute(pkcs_9_at_friendlyName);
                if (nm == null || !nm.getString().equals(name))
                {
                    bagAttrs.setBagAttribute(pkcs_9_at_friendlyName, new DERBMPString(name));
                }

                //
                // make sure we have a local key-id
                //
                if (bagAttrs.getBagAttribute(pkcs_9_at_localKeyId) == null)
                {
                    Certificate             ct = engineGetCertificate(name);

                    bagAttrs.setBagAttribute(pkcs_9_at_localKeyId, createSubjectKeyId(ct.getPublicKey()));
                }

                Enumeration e = bagAttrs.getBagAttributeKeys();

                while (e.hasMoreElements())
                {
                    DERObjectIdentifier oid = (DERObjectIdentifier)e.nextElement();
                    ASN1EncodableVector  kSeq = new ASN1EncodableVector();

                    kSeq.add(oid);
                    kSeq.add(new DERSet(bagAttrs.getBagAttribute(oid)));

                    attrSet = true;

                    kName.add(new DERSequence(kSeq));
                }
            }

            if (!attrSet)
            {
                //
                // set a default friendly name (from the key id) and local id
                //
                ASN1EncodableVector     kSeq = new ASN1EncodableVector();
                Certificate             ct = engineGetCertificate(name);

                kSeq.add(pkcs_9_at_localKeyId);
                kSeq.add(new DERSet(createSubjectKeyId(ct.getPublicKey())));

                kName.add(new DERSequence(kSeq));

                kSeq = new ASN1EncodableVector();

                kSeq.add(pkcs_9_at_friendlyName);
                kSeq.add(new DERSet(new DERBMPString(name)));

                kName.add(new DERSequence(kSeq));
            }

            SafeBag                 kBag = new SafeBag(pkcs8ShroudedKeyBag, kInfo.getDERObject(), new DERSet(kName));
            keyS.add(kBag);
        }

        ByteArrayOutputStream   bOut = new ByteArrayOutputStream();
        DEROutputStream         dOut = new DEROutputStream(bOut);

        dOut.writeObject(new DERSequence(keyS));

        BERConstructedOctetString          keyString = new BERConstructedOctetString(bOut.toByteArray());

        //
        // certficate processing
        //
        byte[]                  cSalt = new byte[SALT_SIZE];

        random.nextBytes(cSalt);

        ASN1EncodableVector  certSeq = new ASN1EncodableVector();
        PKCS12PBEParams         cParams = new PKCS12PBEParams(cSalt, MIN_ITERATIONS);
        AlgorithmIdentifier     cAlgId = new AlgorithmIdentifier(new DERObjectIdentifier(CERT_ALGORITHM), cParams.getDERObject());
        Hashtable               doneCerts = new Hashtable();

        Enumeration cs = keys.keys();
        while (cs.hasMoreElements())
        {
            try
            {
                String              name = (String)cs.nextElement();
                Certificate         cert = engineGetCertificate(name);
                boolean             cAttrSet = false;
                CertBag             cBag = new CertBag(
                                        x509certType,
                                        new DEROctetString(cert.getEncoded()));
                ASN1EncodableVector fName = new ASN1EncodableVector();

                if (cert instanceof PKCS12BagAttributeCarrier)
                {
                    PKCS12BagAttributeCarrier   bagAttrs = (PKCS12BagAttributeCarrier)cert;
                    //
                    // make sure we are using the local alias on store
                    //
                    DERBMPString    nm = (DERBMPString)bagAttrs.getBagAttribute(pkcs_9_at_friendlyName);
                    if (nm == null || !nm.getString().equals(name))
                    {
                        bagAttrs.setBagAttribute(pkcs_9_at_friendlyName, new DERBMPString(name));
                    }

                    //
                    // make sure we have a local key-id
                    //
                    if (bagAttrs.getBagAttribute(pkcs_9_at_localKeyId) == null)
                    {
                        bagAttrs.setBagAttribute(pkcs_9_at_localKeyId, createSubjectKeyId(cert.getPublicKey()));
                    }

                    Enumeration e = bagAttrs.getBagAttributeKeys();

                    while (e.hasMoreElements())
                    {
                        DERObjectIdentifier oid = (DERObjectIdentifier)e.nextElement();
                        ASN1EncodableVector fSeq = new ASN1EncodableVector();

                        fSeq.add(oid);
                        fSeq.add(new DERSet(bagAttrs.getBagAttribute(oid)));
                        fName.add(new DERSequence(fSeq));

                        cAttrSet = true;
                    }
                }

                if (!cAttrSet)
                {
                    ASN1EncodableVector  fSeq = new ASN1EncodableVector();

                    fSeq.add(pkcs_9_at_localKeyId);
                    fSeq.add(new DERSet(createSubjectKeyId(cert.getPublicKey())));
                    fName.add(new DERSequence(fSeq));

                    fSeq = new ASN1EncodableVector();

                    fSeq.add(pkcs_9_at_friendlyName);
                    fSeq.add(new DERSet(new DERBMPString(name)));

                    fName.add(new DERSequence(fSeq));
                }

                SafeBag sBag = new SafeBag(certBag, cBag.getDERObject(), new DERSet(fName));

                certSeq.add(sBag);

                doneCerts.put(cert, cert);
            }
            catch (CertificateEncodingException e)
            {
                throw new IOException("Error encoding certificate: " + e.toString());
            }
        }

        cs = certs.keys();
        while (cs.hasMoreElements())
        {
            try
            {
                String              certId = (String)cs.nextElement();
                Certificate         cert = (Certificate)certs.get(certId);
                boolean             cAttrSet = false;

                if (keys.get(certId) != null)
                {
                    continue;
                }

                CertBag             cBag = new CertBag(
                                        x509certType,
                                        new DEROctetString(cert.getEncoded()));
                ASN1EncodableVector fName = new ASN1EncodableVector();

                if (cert instanceof PKCS12BagAttributeCarrier)
                {
                    PKCS12BagAttributeCarrier   bagAttrs = (PKCS12BagAttributeCarrier)cert;
                    //
                    // make sure we are using the local alias on store
                    //
                    DERBMPString    nm = (DERBMPString)bagAttrs.getBagAttribute(pkcs_9_at_friendlyName);
                    if (nm == null || !nm.getString().equals(certId))
                    {
                        bagAttrs.setBagAttribute(pkcs_9_at_friendlyName, new DERBMPString(certId));
                    }

                    Enumeration e = bagAttrs.getBagAttributeKeys();

                    while (e.hasMoreElements())
                    {
                        DERObjectIdentifier oid = (DERObjectIdentifier)e.nextElement();
                        ASN1EncodableVector fSeq = new ASN1EncodableVector();

                        fSeq.add(oid);
                        fSeq.add(new DERSet(bagAttrs.getBagAttribute(oid)));
                        fName.add(new DERSequence(fSeq));

                        cAttrSet = true;
                    }
                }

                if (!cAttrSet)
                {
                    ASN1EncodableVector  fSeq = new ASN1EncodableVector();

                    fSeq.add(pkcs_9_at_friendlyName);
                    fSeq.add(new DERSet(new DERBMPString(certId)));

                    fName.add(new DERSequence(fSeq));
                }

                SafeBag sBag = new SafeBag(certBag, cBag.getDERObject(), new DERSet(fName));

                certSeq.add(sBag);

                doneCerts.put(cert, cert);
            }
            catch (CertificateEncodingException e)
            {
                throw new IOException("Error encoding certificate: " + e.toString());
            }
        }

        cs = chainCerts.keys();
        while (cs.hasMoreElements())
        {
            try
            {
                CertId              certId = (CertId)cs.nextElement();
                Certificate         cert = (Certificate)chainCerts.get(certId);

                if (doneCerts.get(cert) != null)
                {
                    continue;
                }

                CertBag             cBag = new CertBag(
                                        x509certType,
                                        new DEROctetString(cert.getEncoded()));
                ASN1EncodableVector fName = new ASN1EncodableVector();

                if (cert instanceof PKCS12BagAttributeCarrier)
                {
                    PKCS12BagAttributeCarrier   bagAttrs = (PKCS12BagAttributeCarrier)cert;
                    Enumeration e = bagAttrs.getBagAttributeKeys();

                    while (e.hasMoreElements())
                    {
                        DERObjectIdentifier oid = (DERObjectIdentifier)e.nextElement();
                        ASN1EncodableVector fSeq = new ASN1EncodableVector();

                        fSeq.add(oid);
                        fSeq.add(new DERSet(bagAttrs.getBagAttribute(oid)));
                        fName.add(new DERSequence(fSeq));
                    }
                }

                SafeBag sBag = new SafeBag(certBag, cBag.getDERObject(), new DERSet(fName));

                certSeq.add(sBag);
            }
            catch (CertificateEncodingException e)
            {
                throw new IOException("Error encoding certificate: " + e.toString());
            }
        }

        bOut.reset();

        dOut = new DEROutputStream(bOut);

        dOut.writeObject(new DERSequence(certSeq));

        dOut.close();

        byte[]                  certBytes = encryptData(CERT_ALGORITHM, bOut.toByteArray(), cParams, password);
        EncryptedData           cInfo = new EncryptedData(data, cAlgId, new BERConstructedOctetString(certBytes));

        c[0] = new ContentInfo(data, keyString);

        c[1] = new ContentInfo(encryptedData, cInfo.getDERObject());

        AuthenticatedSafe   auth = new AuthenticatedSafe(c);

        bOut.reset();

        BEROutputStream         berOut = new BEROutputStream(bOut);

        berOut.writeObject(auth);

        byte[]              pkg = bOut.toByteArray();

        ContentInfo         mainInfo = new ContentInfo(data, new BERConstructedOctetString(pkg));

        //
        // create the mac
        //
        byte[]                      mSalt = new byte[20];
        int                         itCount = MIN_ITERATIONS;

        random.nextBytes(mSalt);
    
        byte[]  data = ((ASN1OctetString)mainInfo.getContent()).getOctets();

        MacData                 mData;

        try
        {
            Mac                 mac = Mac.getInstance(id_SHA1.getId(), "BC");
            SecretKeyFactory    keyFact = SecretKeyFactory.getInstance(id_SHA1.getId(), "BC");
            PBEParameterSpec    defParams = new PBEParameterSpec(mSalt, itCount);
            PBEKeySpec          pbeSpec = new PBEKeySpec(password);

            mac.init(keyFact.generateSecret(pbeSpec), defParams);

            mac.update(data);

            byte[]      res = mac.doFinal();

            AlgorithmIdentifier     algId = new AlgorithmIdentifier(id_SHA1, new DERNull());
            DigestInfo              dInfo = new DigestInfo(algId, res);

            mData = new MacData(dInfo, mSalt, itCount);
        }
        catch (Exception e)
        {
            throw new IOException("error constructing MAC: " + e.toString());
        }
        
        //
        // output the Pfx
        //
        Pfx                 pfx = new Pfx(mainInfo, mData);

        berOut = new BEROutputStream(stream);

        berOut.writeObject(pfx);
    }

    public static class BCPKCS12KeyStore
        extends JDKPKCS12KeyStore
    {
        public BCPKCS12KeyStore()
        {
            super("BC");
        }
    }

    public static class DefPKCS12KeyStore
        extends JDKPKCS12KeyStore
    {
        public DefPKCS12KeyStore()
        {
            super(null);
        }
    }

    private static class IgnoresCaseHashtable
    {
        private Hashtable orig = new Hashtable();
        private Hashtable keys = new Hashtable();

        public void put(String key, Object value)
        {
            keys.put(Strings.toLowerCase(key), key);
            orig.put(key, value);
        }

        public Enumeration keys()
        {
            return orig.keys();
        }

        public Object remove(String alias)
        {
            String k = (String)keys.remove(Strings.toLowerCase(alias));
            if (k == null)
            {
                return null;
            }

            return orig.remove(k);
        }

        public Object get(String alias)
        {
            String k = (String)keys.get(Strings.toLowerCase(alias));
            if (k == null)
            {
                return null;
            }
            
            return orig.get(k);
        }

        public Enumeration elements()
        {
            return orig.elements();
        }
    }
}
