using System;
using System.Collections;
using System.IO;
using System.Text;

using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Oiw;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Asn1.Utilities;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;
using Org.BouncyCastle.Utilities.Collections;
using Org.BouncyCastle.Utilities.Encoders;
using Org.BouncyCastle.X509;

namespace Org.BouncyCastle.Pkcs
{
    public class Pkcs12Store
    {
        public const string IgnoreUselessPasswordProperty = "Org.BouncyCastle.Pkcs12.IgnoreUselessPassword";

        private readonly IgnoresCaseHashtable   keys = new IgnoresCaseHashtable();
        private readonly IDictionary            localIds = Platform.CreateHashtable();
        private readonly IgnoresCaseHashtable   certs = new IgnoresCaseHashtable();
        private readonly IDictionary            chainCerts = Platform.CreateHashtable();
        private readonly IDictionary            keyCerts = Platform.CreateHashtable();
        private readonly DerObjectIdentifier    keyAlgorithm;
        private readonly DerObjectIdentifier    keyPrfAlgorithm;
        private readonly DerObjectIdentifier    certAlgorithm;
        private readonly DerObjectIdentifier    certPrfAlgorithm;
        private readonly bool                   useDerEncoding;

        private AsymmetricKeyEntry unmarkedKeyEntry = null;

        private const int MinIterations = 1024;
        private const int SaltSize = 20;

        private static SubjectKeyIdentifier CreateSubjectKeyID(
            AsymmetricKeyParameter pubKey)
        {
            return new SubjectKeyIdentifier(
                SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(pubKey));
        }

        internal class CertId
        {
            private readonly byte[] id;

            internal CertId(
                AsymmetricKeyParameter pubKey)
            {
                this.id = CreateSubjectKeyID(pubKey).GetKeyIdentifier();
            }

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

            internal byte[] Id
            {
                get { return id; }
            }

            public override int GetHashCode()
            {
                return Arrays.GetHashCode(id);
            }

            public override bool Equals(
                object obj)
            {
                if (obj == this)
                    return true;

                CertId other = obj as CertId;

                if (other == null)
                    return false;

                return Arrays.AreEqual(id, other.id);
            }
        }

        internal Pkcs12Store(
            DerObjectIdentifier keyAlgorithm,
            DerObjectIdentifier certAlgorithm,
            bool                useDerEncoding)
        {
            this.keyAlgorithm = keyAlgorithm;
            this.keyPrfAlgorithm = null;
            this.certAlgorithm = certAlgorithm;
            this.certPrfAlgorithm = null;
            this.useDerEncoding = useDerEncoding;
        }

        internal Pkcs12Store(
            DerObjectIdentifier keyAlgorithm,
            DerObjectIdentifier keyPrfAlgorithm,
            DerObjectIdentifier certAlgorithm,
            DerObjectIdentifier certPrfAlgorithm,
            bool useDerEncoding)
        {
            this.keyAlgorithm = keyAlgorithm;
            this.keyPrfAlgorithm = keyPrfAlgorithm;
            this.certAlgorithm = certAlgorithm;
            this.certPrfAlgorithm = certPrfAlgorithm;
            this.useDerEncoding = useDerEncoding;
        }

        // TODO Consider making obsolete
        //      [Obsolete("Use 'Pkcs12StoreBuilder' instead")]
        public Pkcs12Store()
            : this(PkcsObjectIdentifiers.PbeWithShaAnd3KeyTripleDesCbc,
                PkcsObjectIdentifiers.PbewithShaAnd40BitRC2Cbc, false)
        {
        }

        // TODO Consider making obsolete
//      [Obsolete("Use 'Pkcs12StoreBuilder' and 'Load' method instead")]
        public Pkcs12Store(
            Stream  input,
            char[]  password)
            : this()
        {
            Load(input, password);
        }

        protected virtual void LoadKeyBag(PrivateKeyInfo privKeyInfo, Asn1Set bagAttributes)
        {
            AsymmetricKeyParameter privKey = PrivateKeyFactory.CreateKey(privKeyInfo);

            IDictionary attributes = Platform.CreateHashtable();
            AsymmetricKeyEntry keyEntry = new AsymmetricKeyEntry(privKey, attributes);

            string alias = null;
            Asn1OctetString localId = null;

            if (bagAttributes != null)
            {
                foreach (Asn1Sequence sq in bagAttributes)
                {
                    DerObjectIdentifier aOid = DerObjectIdentifier.GetInstance(sq[0]);
                    Asn1Set attrSet = Asn1Set.GetInstance(sq[1]);
                    Asn1Encodable attr = null;

                    if (attrSet.Count > 0)
                    {
                        // TODO We should be adding all attributes in the set
                        attr = attrSet[0];

                        // TODO We might want to "merge" attribute sets with
                        // the same OID - currently, differing values give an error
                        if (attributes.Contains(aOid.Id))
                        {
                            // OK, but the value has to be the same
                            if (!attributes[aOid.Id].Equals(attr))
                                throw new IOException("attempt to add existing attribute with different value");
                        }
                        else
                        {
                            attributes.Add(aOid.Id, attr);
                        }

                        if (aOid.Equals(PkcsObjectIdentifiers.Pkcs9AtFriendlyName))
                        {
                            alias = ((DerBmpString)attr).GetString();
                            // TODO Do these in a separate loop, just collect aliases here
                            keys[alias] = keyEntry;
                        }
                        else if (aOid.Equals(PkcsObjectIdentifiers.Pkcs9AtLocalKeyID))
                        {
                            localId = (Asn1OctetString)attr;
                        }
                    }
                }
            }

            if (localId != null)
            {
                string name = Hex.ToHexString(localId.GetOctets());

                if (alias == null)
                {
                    keys[name] = keyEntry;
                }
                else
                {
                    // TODO There may have been more than one alias
                    localIds[alias] = name;
                }
            }
            else
            {
                unmarkedKeyEntry = keyEntry;
            }
        }

        protected virtual void LoadPkcs8ShroudedKeyBag(EncryptedPrivateKeyInfo encPrivKeyInfo, Asn1Set bagAttributes,
            char[] password, bool wrongPkcs12Zero)
        {
            if (password != null)
            {
                PrivateKeyInfo privInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(
                    password, wrongPkcs12Zero, encPrivKeyInfo);

                LoadKeyBag(privInfo, bagAttributes);
            }
        }

        public void Load(
            Stream  input,
            char[]  password)
        {
            if (input == null)
                throw new ArgumentNullException("input");

            Pfx bag = Pfx.GetInstance(Asn1Object.FromStream(input));
            ContentInfo info = bag.AuthSafe;
            bool wrongPkcs12Zero = false;

            if (bag.MacData != null) // check the mac code
            {
                if (password == null)
                    throw new ArgumentNullException("password", "no password supplied when one expected");

                MacData mData = bag.MacData;
                DigestInfo dInfo = mData.Mac;
                AlgorithmIdentifier algId = dInfo.AlgorithmID;
                byte[] salt = mData.GetSalt();
                int itCount = mData.IterationCount.IntValue;

                byte[] data = Asn1OctetString.GetInstance(info.Content).GetOctets();

                byte[] mac = CalculatePbeMac(algId.Algorithm, salt, itCount, password, false, data);
                byte[] dig = dInfo.GetDigest();

                if (!Arrays.ConstantTimeAreEqual(mac, dig))
                {
                    if (password.Length > 0)
                        throw new IOException("PKCS12 key store MAC invalid - wrong password or corrupted file.");

                    // Try with incorrect zero length password
                    mac = CalculatePbeMac(algId.Algorithm, salt, itCount, password, true, data);

                    if (!Arrays.ConstantTimeAreEqual(mac, dig))
                        throw new IOException("PKCS12 key store MAC invalid - wrong password or corrupted file.");

                    wrongPkcs12Zero = true;
                }
            }
            else if (password != null)
            {
                string ignoreProperty = Platform.GetEnvironmentVariable(IgnoreUselessPasswordProperty);
                bool ignore = ignoreProperty != null && Platform.EqualsIgnoreCase("true", ignoreProperty);

                if (!ignore)
                {
                    throw new IOException("password supplied for keystore that does not require one");
                }
            }

            keys.Clear();
            localIds.Clear();
            unmarkedKeyEntry = null;

            IList certBags = Platform.CreateArrayList();

            if (info.ContentType.Equals(PkcsObjectIdentifiers.Data))
            {
                Asn1OctetString content = Asn1OctetString.GetInstance(info.Content);
                AuthenticatedSafe authSafe = AuthenticatedSafe.GetInstance(content.GetOctets());
                ContentInfo[] cis = authSafe.GetContentInfo();

                foreach (ContentInfo ci in cis)
                {
                    DerObjectIdentifier oid = ci.ContentType;

                    byte[] octets = null;
                    if (oid.Equals(PkcsObjectIdentifiers.Data))
                    {
                        octets = Asn1OctetString.GetInstance(ci.Content).GetOctets();
                    }
                    else if (oid.Equals(PkcsObjectIdentifiers.EncryptedData))
                    {
                        if (password != null)
                        {
                            EncryptedData d = EncryptedData.GetInstance(ci.Content);
                            octets = CryptPbeData(false, d.EncryptionAlgorithm,
                                password, wrongPkcs12Zero, d.Content.GetOctets());
                        }
                    }
                    else
                    {
                        // TODO Other data types
                    }

                    if (octets != null)
                    {
                        Asn1Sequence seq = Asn1Sequence.GetInstance(octets);

                        foreach (Asn1Sequence subSeq in seq)
                        {
                            SafeBag b = SafeBag.GetInstance(subSeq);

                            if (b.BagID.Equals(PkcsObjectIdentifiers.CertBag))
                            {
                                certBags.Add(b);
                            }
                            else if (b.BagID.Equals(PkcsObjectIdentifiers.Pkcs8ShroudedKeyBag))
                            {
                                LoadPkcs8ShroudedKeyBag(EncryptedPrivateKeyInfo.GetInstance(b.BagValue),
                                    b.BagAttributes, password, wrongPkcs12Zero);
                            }
                            else if (b.BagID.Equals(PkcsObjectIdentifiers.KeyBag))
                            {
                                LoadKeyBag(PrivateKeyInfo.GetInstance(b.BagValue), b.BagAttributes);
                            }
                            else
                            {
                                // TODO Other bag types
                            }
                        }
                    }
                }
            }

            certs.Clear();
            chainCerts.Clear();
            keyCerts.Clear();

            foreach (SafeBag b in certBags)
            {
                CertBag certBag = CertBag.GetInstance(b.BagValue);
                byte[] octets = ((Asn1OctetString)certBag.CertValue).GetOctets();
                X509Certificate cert = new X509CertificateParser().ReadCertificate(octets);

                //
                // set the attributes
                //
                IDictionary attributes = Platform.CreateHashtable();
                Asn1OctetString localId = null;
                string alias = null;

                if (b.BagAttributes != null)
                {
                    foreach (Asn1Sequence sq in b.BagAttributes)
                    {
                        DerObjectIdentifier aOid = DerObjectIdentifier.GetInstance(sq[0]);
                        Asn1Set attrSet = Asn1Set.GetInstance(sq[1]);

                        if (attrSet.Count > 0)
                        {
                            // TODO We should be adding all attributes in the set
                            Asn1Encodable attr = attrSet[0];

                            // TODO We might want to "merge" attribute sets with
                            // the same OID - currently, differing values give an error
                            if (attributes.Contains(aOid.Id))
                            {
                                // we've found more than one - one might be incorrect
                                if (aOid.Equals(PkcsObjectIdentifiers.Pkcs9AtLocalKeyID))
                                {
                                    String id = Hex.ToHexString(Asn1OctetString.GetInstance(attr).GetOctets());
                                    if (!(keys[id] != null || localIds[id] != null))
                                    {
                                        continue; // ignore this one - it's not valid
                                    }
                                }

                                // OK, but the value has to be the same
                                if (!attributes[aOid.Id].Equals(attr))
                                {
                                    throw new IOException("attempt to add existing attribute with different value");
                                }
                            }
                            else
                            {
                                attributes.Add(aOid.Id, attr);
                            }

                            if (aOid.Equals(PkcsObjectIdentifiers.Pkcs9AtFriendlyName))
                            {
                                alias = ((DerBmpString)attr).GetString();
                            }
                            else if (aOid.Equals(PkcsObjectIdentifiers.Pkcs9AtLocalKeyID))
                            {
                                localId = (Asn1OctetString)attr;
                            }
                        }
                    }
                }

                CertId certId = new CertId(cert.GetPublicKey());
                X509CertificateEntry certEntry = new X509CertificateEntry(cert, attributes);

                chainCerts[certId] = certEntry;

                if (unmarkedKeyEntry != null)
                {
                    if (keyCerts.Count == 0)
                    {
                        string name = Hex.ToHexString(certId.Id);

                        keyCerts[name] = certEntry;
                        keys[name] = unmarkedKeyEntry;
                    }
                    else
                    {
                        keys["unmarked"] = unmarkedKeyEntry;
                    }
                }
                else
                {
                    if (localId != null)
                    {
                        string name = Hex.ToHexString(localId.GetOctets());

                        keyCerts[name] = certEntry;
                    }

                    if (alias != null)
                    {
                        // TODO There may have been more than one alias
                        certs[alias] = certEntry;
                    }
                }
            }
        }

        public AsymmetricKeyEntry GetKey(
            string alias)
        {
            if (alias == null)
                throw new ArgumentNullException("alias");

            return (AsymmetricKeyEntry)keys[alias];
        }

        public bool IsCertificateEntry(
            string alias)
        {
            if (alias == null)
                throw new ArgumentNullException("alias");

            return (certs[alias] != null && keys[alias] == null);
        }

        public bool IsKeyEntry(
            string alias)
        {
            if (alias == null)
                throw new ArgumentNullException("alias");

            return (keys[alias] != null);
        }

        private IDictionary GetAliasesTable()
        {
            IDictionary tab = Platform.CreateHashtable();

            foreach (string key in certs.Keys)
            {
                tab[key] = "cert";
            }

            foreach (string a in keys.Keys)
            {
                if (tab[a] == null)
                {
                    tab[a] = "key";
                }
            }

            return tab;
        }

        public IEnumerable Aliases
        {
            get { return new EnumerableProxy(GetAliasesTable().Keys); }
        }

        public bool ContainsAlias(
            string alias)
        {
            return certs[alias] != null || keys[alias] != null;
        }

        /**
         * simply return the cert entry for the private key
         */
        public X509CertificateEntry GetCertificate(
            string alias)
        {
            if (alias == null)
                throw new ArgumentNullException("alias");

            X509CertificateEntry c = (X509CertificateEntry) certs[alias];

            //
            // look up the key table - and try the local key id
            //
            if (c == null)
            {
                string id = (string)localIds[alias];
                if (id != null)
                {
                    c = (X509CertificateEntry)keyCerts[id];
                }
                else
                {
                    c = (X509CertificateEntry)keyCerts[alias];
                }
            }

            return c;
        }

        public string GetCertificateAlias(
            X509Certificate cert)
        {
            if (cert == null)
                throw new ArgumentNullException("cert");

            foreach (DictionaryEntry entry in certs)
            {
                X509CertificateEntry entryValue = (X509CertificateEntry) entry.Value;
                if (entryValue.Certificate.Equals(cert))
                {
                    return (string) entry.Key;
                }
            }

            foreach (DictionaryEntry entry in keyCerts)
            {
                X509CertificateEntry entryValue = (X509CertificateEntry) entry.Value;
                if (entryValue.Certificate.Equals(cert))
                {
                    return (string) entry.Key;
                }
            }

            return null;
        }

        public X509CertificateEntry[] GetCertificateChain(
            string alias)
        {
            if (alias == null)
                throw new ArgumentNullException("alias");

            if (!IsKeyEntry(alias))
            {
                return null;
            }

            X509CertificateEntry c = GetCertificate(alias);

            if (c != null)
            {
                IList cs = Platform.CreateArrayList();

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

                    Asn1OctetString akiValue = x509c.GetExtensionValue(X509Extensions.AuthorityKeyIdentifier);
                    if (akiValue != null)
                    {
                        AuthorityKeyIdentifier aki = AuthorityKeyIdentifier.GetInstance(akiValue.GetOctets());

                        byte[] keyID = aki.GetKeyIdentifier();
                        if (keyID != null)
                        {
                            nextC = (X509CertificateEntry)chainCerts[new CertId(keyID)];
                        }
                    }

                    if (nextC == null)
                    {
                        //
                        // no authority key id, try the Issuer DN
                        //
                        X509Name i = x509c.IssuerDN;
                        X509Name s = x509c.SubjectDN;

                        if (!i.Equivalent(s))
                        {
                            foreach (CertId certId in chainCerts.Keys)
                            {
                                X509CertificateEntry x509CertEntry = (X509CertificateEntry) chainCerts[certId];

                                X509Certificate crt = x509CertEntry.Certificate;

                                X509Name sub = crt.SubjectDN;
                                if (sub.Equivalent(i))
                                {
                                    try
                                    {
                                        x509c.Verify(crt.GetPublicKey());

                                        nextC = x509CertEntry;
                                        break;
                                    }
                                    catch (InvalidKeyException)
                                    {
                                        // TODO What if it doesn't verify?
                                    }
                                }
                            }
                        }
                    }

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

                X509CertificateEntry[] result = new X509CertificateEntry[cs.Count];
                for (int i = 0; i < cs.Count; ++i)
                {
                    result[i] = (X509CertificateEntry)cs[i];
                }
                return result;
            }

            return null;
        }

        public void SetCertificateEntry(
            string                  alias,
            X509CertificateEntry    certEntry)
        {
            if (alias == null)
                throw new ArgumentNullException("alias");
            if (certEntry == null)
                throw new ArgumentNullException("certEntry");
            if (keys[alias] != null)
                throw new ArgumentException("There is a key entry with the name " + alias + ".");

            certs[alias] = certEntry;
            chainCerts[new CertId(certEntry.Certificate.GetPublicKey())] = certEntry;
        }

        public void SetKeyEntry(
            string                  alias,
            AsymmetricKeyEntry      keyEntry,
            X509CertificateEntry[]  chain)
        {
            if (alias == null)
                throw new ArgumentNullException("alias");
            if (keyEntry == null)
                throw new ArgumentNullException("keyEntry");
            if (keyEntry.Key.IsPrivate && (chain == null))
                throw new ArgumentException("No certificate chain for private key");

            if (keys[alias] != null)
            {
                DeleteEntry(alias);
            }

            keys[alias] = keyEntry;
            certs[alias] = chain[0];

            for (int i = 0; i != chain.Length; i++)
            {
                chainCerts[new CertId(chain[i].Certificate.GetPublicKey())] = chain[i];
            }
        }

        public void DeleteEntry(
            string alias)
        {
            if (alias == null)
                throw new ArgumentNullException("alias");

            AsymmetricKeyEntry k = (AsymmetricKeyEntry)keys[alias];
            if (k != null)
            {
                keys.Remove(alias);
            }

            X509CertificateEntry c = (X509CertificateEntry)certs[alias];

            if (c != null)
            {
                certs.Remove(alias);
                chainCerts.Remove(new CertId(c.Certificate.GetPublicKey()));
            }

            if (k != null)
            {
                string id = (string)localIds[alias];
                if (id != null)
                {
                    localIds.Remove(alias);
                    c = (X509CertificateEntry)keyCerts[id];
                }
                if (c != null)
                {
                    keyCerts.Remove(id);
                    chainCerts.Remove(new CertId(c.Certificate.GetPublicKey()));
                }
            }

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

        public bool IsEntryOfType(
            string  alias,
            Type    entryType)
        {
            if (entryType == typeof(X509CertificateEntry))
                return IsCertificateEntry(alias);

            if (entryType == typeof(AsymmetricKeyEntry))
                return IsKeyEntry(alias) && GetCertificate(alias) != null;

            return false;
        }

        [Obsolete("Use 'Count' property instead")]
        public int Size()
        {
            return Count;
        }

        public int Count
        {
            // TODO Seems a little inefficient
            get { return GetAliasesTable().Count; }
        }

        public void Save(
            Stream          stream,
            char[]          password,
            SecureRandom    random)
        {
            if (stream == null)
                throw new ArgumentNullException("stream");
            if (random == null)
                throw new ArgumentNullException("random");

            //
            // handle the keys
            //
            Asn1EncodableVector keyBags = new Asn1EncodableVector();
            foreach (string name in keys.Keys)
            {
                byte[] kSalt = new byte[SaltSize];
                random.NextBytes(kSalt);

                AsymmetricKeyEntry privKey = (AsymmetricKeyEntry)keys[name];

                DerObjectIdentifier bagOid;
                Asn1Encodable bagData;

                if (password == null)
                {
                    bagOid = PkcsObjectIdentifiers.KeyBag;
                    bagData = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privKey.Key);
                }
                else
                {
                    bagOid = PkcsObjectIdentifiers.Pkcs8ShroudedKeyBag;
                    if (keyPrfAlgorithm != null)
                    {
                        bagData = EncryptedPrivateKeyInfoFactory.CreateEncryptedPrivateKeyInfo(
                                        keyAlgorithm, keyPrfAlgorithm, password, kSalt, MinIterations, random, privKey.Key);
                    }
                    else
                    {
                        bagData = EncryptedPrivateKeyInfoFactory.CreateEncryptedPrivateKeyInfo(
                                            keyAlgorithm, password, kSalt, MinIterations, privKey.Key);
                    }
                }

                Asn1EncodableVector kName = new Asn1EncodableVector();

                foreach (string oid in privKey.BagAttributeKeys)
                {
                    Asn1Encodable entry = privKey[oid];

                    // NB: Ignore any existing FriendlyName
                    if (oid.Equals(PkcsObjectIdentifiers.Pkcs9AtFriendlyName.Id))
                        continue;

                    kName.Add(
                        new DerSequence(
                            new DerObjectIdentifier(oid),
                            new DerSet(entry)));
                }

                //
                // make sure we are using the local alias on store
                //
                // NB: We always set the FriendlyName based on 'name'
                //if (privKey[PkcsObjectIdentifiers.Pkcs9AtFriendlyName] == null)
                {
                    kName.Add(
                        new DerSequence(
                            PkcsObjectIdentifiers.Pkcs9AtFriendlyName,
                            new DerSet(new DerBmpString(name))));
                }

                //
                // make sure we have a local key-id
                //
                if (privKey[PkcsObjectIdentifiers.Pkcs9AtLocalKeyID] == null)
                {
                    X509CertificateEntry ct = GetCertificate(name);
                    AsymmetricKeyParameter pubKey = ct.Certificate.GetPublicKey();
                    SubjectKeyIdentifier subjectKeyID = CreateSubjectKeyID(pubKey);

                    kName.Add(
                        new DerSequence(
                            PkcsObjectIdentifiers.Pkcs9AtLocalKeyID,
                            new DerSet(subjectKeyID)));
                }

                keyBags.Add(new SafeBag(bagOid, bagData.ToAsn1Object(), new DerSet(kName)));
            }

            byte[] keyBagsEncoding = new DerSequence(keyBags).GetDerEncoded();
            ContentInfo keysInfo = new ContentInfo(PkcsObjectIdentifiers.Data, new BerOctetString(keyBagsEncoding));

            //
            // certificate processing
            //
            byte[] cSalt = new byte[SaltSize];

            random.NextBytes(cSalt);

            Asn1EncodableVector certBags = new Asn1EncodableVector();
            Pkcs12PbeParams     cParams = new Pkcs12PbeParams(cSalt, MinIterations);
            AlgorithmIdentifier cAlgId = new AlgorithmIdentifier(certAlgorithm, cParams.ToAsn1Object());
            ISet                doneCerts = new HashSet();

            foreach (string name in keys.Keys)
            {
                X509CertificateEntry certEntry = GetCertificate(name);
                CertBag cBag = new CertBag(
                    PkcsObjectIdentifiers.X509Certificate,
                    new DerOctetString(certEntry.Certificate.GetEncoded()));

                Asn1EncodableVector fName = new Asn1EncodableVector();

                foreach (string oid in certEntry.BagAttributeKeys)
                {
                    Asn1Encodable entry = certEntry[oid];

                    // NB: Ignore any existing FriendlyName
                    if (oid.Equals(PkcsObjectIdentifiers.Pkcs9AtFriendlyName.Id))
                        continue;

                    fName.Add(
                        new DerSequence(
                            new DerObjectIdentifier(oid),
                            new DerSet(entry)));
                }

                //
                // make sure we are using the local alias on store
                //
                // NB: We always set the FriendlyName based on 'name'
                //if (certEntry[PkcsObjectIdentifiers.Pkcs9AtFriendlyName] == null)
                {
                    fName.Add(
                        new DerSequence(
                            PkcsObjectIdentifiers.Pkcs9AtFriendlyName,
                            new DerSet(new DerBmpString(name))));
                }

                //
                // make sure we have a local key-id
                //
                if (certEntry[PkcsObjectIdentifiers.Pkcs9AtLocalKeyID] == null)
                {
                    AsymmetricKeyParameter pubKey = certEntry.Certificate.GetPublicKey();
                    SubjectKeyIdentifier subjectKeyID = CreateSubjectKeyID(pubKey);

                    fName.Add(
                        new DerSequence(
                            PkcsObjectIdentifiers.Pkcs9AtLocalKeyID,
                            new DerSet(subjectKeyID)));
                }

                certBags.Add(new SafeBag(PkcsObjectIdentifiers.CertBag, cBag.ToAsn1Object(), new DerSet(fName)));

                doneCerts.Add(certEntry.Certificate);
            }

            foreach (string certId in certs.Keys)
            {
                X509CertificateEntry cert = (X509CertificateEntry)certs[certId];

                if (keys[certId] != null)
                    continue;

                CertBag cBag = new CertBag(
                    PkcsObjectIdentifiers.X509Certificate,
                    new DerOctetString(cert.Certificate.GetEncoded()));

                Asn1EncodableVector fName = new Asn1EncodableVector();

                foreach (string oid in cert.BagAttributeKeys)
                {
                    // a certificate not immediately linked to a key doesn't require
                    // a localKeyID and will confuse some PKCS12 implementations.
                    //
                    // If we find one, we'll prune it out.
                    if (oid.Equals(PkcsObjectIdentifiers.Pkcs9AtLocalKeyID.Id))
                        continue;

                    Asn1Encodable entry = cert[oid];

                    // NB: Ignore any existing FriendlyName
                    if (oid.Equals(PkcsObjectIdentifiers.Pkcs9AtFriendlyName.Id))
                        continue;

                    fName.Add(
                        new DerSequence(
                            new DerObjectIdentifier(oid),
                            new DerSet(entry)));
                }

                //
                // make sure we are using the local alias on store
                //
                // NB: We always set the FriendlyName based on 'certId'
                //if (cert[PkcsObjectIdentifiers.Pkcs9AtFriendlyName] == null)
                {
                    fName.Add(
                        new DerSequence(
                            PkcsObjectIdentifiers.Pkcs9AtFriendlyName,
                            new DerSet(new DerBmpString(certId))));
                }

                certBags.Add(new SafeBag(PkcsObjectIdentifiers.CertBag, cBag.ToAsn1Object(), new DerSet(fName)));

                doneCerts.Add(cert.Certificate);
            }

            foreach (CertId certId in chainCerts.Keys)
            {
                X509CertificateEntry cert = (X509CertificateEntry)chainCerts[certId];

                if (doneCerts.Contains(cert.Certificate))
                    continue;

                CertBag cBag = new CertBag(
                    PkcsObjectIdentifiers.X509Certificate,
                    new DerOctetString(cert.Certificate.GetEncoded()));

                Asn1EncodableVector fName = new Asn1EncodableVector();

                foreach (string oid in cert.BagAttributeKeys)
                {
                    // a certificate not immediately linked to a key doesn't require
                    // a localKeyID and will confuse some PKCS12 implementations.
                    //
                    // If we find one, we'll prune it out.
                    if (oid.Equals(PkcsObjectIdentifiers.Pkcs9AtLocalKeyID.Id))
                        continue;

                    fName.Add(
                        new DerSequence(
                            new DerObjectIdentifier(oid),
                            new DerSet(cert[oid])));
                }

                certBags.Add(new SafeBag(PkcsObjectIdentifiers.CertBag, cBag.ToAsn1Object(), new DerSet(fName)));
            }

            byte[] certBagsEncoding = new DerSequence(certBags).GetDerEncoded();

            ContentInfo certsInfo;
            if (password == null || certAlgorithm == null)
            {
                certsInfo = new ContentInfo(PkcsObjectIdentifiers.Data, new BerOctetString(certBagsEncoding));
            }
            else
            {
                byte[] certBytes = CryptPbeData(true, cAlgId, password, false, certBagsEncoding);
                EncryptedData cInfo = new EncryptedData(PkcsObjectIdentifiers.Data, cAlgId, new BerOctetString(certBytes));
                certsInfo = new ContentInfo(PkcsObjectIdentifiers.EncryptedData, cInfo.ToAsn1Object());
            }

            ContentInfo[] info = new ContentInfo[]{ keysInfo, certsInfo };

            byte[] data = new AuthenticatedSafe(info).GetEncoded(
                useDerEncoding ? Asn1Encodable.Der : Asn1Encodable.Ber);

            ContentInfo mainInfo = new ContentInfo(PkcsObjectIdentifiers.Data, new BerOctetString(data));

            //
            // create the mac
            //
            MacData macData = null;
            if (password != null)
            {
                byte[] mSalt = new byte[20];
                random.NextBytes(mSalt);

                byte[] mac = CalculatePbeMac(OiwObjectIdentifiers.IdSha1,
                    mSalt, MinIterations, password, false, data);

                AlgorithmIdentifier algId = new AlgorithmIdentifier(
                    OiwObjectIdentifiers.IdSha1, DerNull.Instance);
                DigestInfo dInfo = new DigestInfo(algId, mac);

                macData = new MacData(dInfo, mSalt, MinIterations);
            }

            //
            // output the Pfx
            //
            Pfx pfx = new Pfx(mainInfo, macData);

            pfx.EncodeTo(stream, useDerEncoding ? Asn1Encodable.Der : Asn1Encodable.Ber);
        }

        internal static byte[] CalculatePbeMac(
            DerObjectIdentifier oid,
            byte[]              salt,
            int                 itCount,
            char[]              password,
            bool                wrongPkcs12Zero,
            byte[]              data)
        {
            Asn1Encodable asn1Params = PbeUtilities.GenerateAlgorithmParameters(
                oid, salt, itCount);
            ICipherParameters cipherParams = PbeUtilities.GenerateCipherParameters(
                oid, password, wrongPkcs12Zero, asn1Params);

            IMac mac = (IMac) PbeUtilities.CreateEngine(oid);
            mac.Init(cipherParams);
            return MacUtilities.DoFinal(mac, data);
        }

        private static byte[] CryptPbeData(
            bool                forEncryption,
            AlgorithmIdentifier algId,
            char[]              password,
            bool                wrongPkcs12Zero,
            byte[]              data)
        {
            IBufferedCipher cipher = PbeUtilities.CreateEngine(algId) as IBufferedCipher;

            if (cipher == null)
                throw new Exception("Unknown encryption algorithm: " + algId.Algorithm);

            if (algId.Algorithm.Equals(PkcsObjectIdentifiers.IdPbeS2))
            {
                PbeS2Parameters pbeParameters = PbeS2Parameters.GetInstance(algId.Parameters);
                ICipherParameters cipherParams = PbeUtilities.GenerateCipherParameters(
                    algId.Algorithm, password, pbeParameters);
                cipher.Init(forEncryption, cipherParams);
                return cipher.DoFinal(data);
            }
            else
            {
                Pkcs12PbeParams pbeParameters = Pkcs12PbeParams.GetInstance(algId.Parameters);
                ICipherParameters cipherParams = PbeUtilities.GenerateCipherParameters(
                    algId.Algorithm, password, wrongPkcs12Zero, pbeParameters);
                cipher.Init(forEncryption, cipherParams);
                return cipher.DoFinal(data);
            }
        }

        private class IgnoresCaseHashtable
            : IEnumerable
        {
            private readonly IDictionary orig = Platform.CreateHashtable();
            private readonly IDictionary keys = Platform.CreateHashtable();

            public void Clear()
            {
                orig.Clear();
                keys.Clear();
            }

            public IEnumerator GetEnumerator()
            {
                return orig.GetEnumerator();
            }

            public ICollection Keys
            {
                get { return orig.Keys; }
            }

            public object Remove(
                string alias)
            {
                string upper = Platform.ToUpperInvariant(alias);
                string k = (string)keys[upper];

                if (k == null)
                    return null;

                keys.Remove(upper);

                object o = orig[k];
                orig.Remove(k);
                return o;
            }

            public object this[
                string alias]
            {
                get
                {
                    string upper = Platform.ToUpperInvariant(alias);
                    string k = (string)keys[upper];

                    if (k == null)
                        return null;

                    return orig[k];
                }
                set
                {
                    string upper = Platform.ToUpperInvariant(alias);
                    string k = (string)keys[upper];
                    if (k != null)
                    {
                        orig.Remove(k);
                    }
                    keys[upper] = alias;
                    orig[alias] = value;
                }
            }

            public ICollection Values
            {
                get { return orig.Values; }
            }

            public int Count
            {
                get { return orig.Count; }
            }
        }
    }
}
