// ==++==
// 
//   Copyright (c) Microsoft Corporation.  All rights reserved.
// 
// ==--==
// <OWNER>[....]</OWNER>
// 

//
// Hash
//
// Evidence corresponding to a hash of the assembly bits.
//

using System;
using System.Diagnostics.Contracts;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Security.Util;
using Microsoft.Win32.SafeHandles;

namespace System.Security.Policy
{
    [Serializable]
    [ComVisible(true)]
    public sealed class Hash : EvidenceBase, ISerializable
    {
        private RuntimeAssembly m_assembly;
        private Dictionary<Type, byte[]> m_hashes;
        private WeakReference m_rawData;

        /// <summary>
        ///     Deserialize a serialized hash evidence object
        /// </summary>
        [SecurityCritical]
        internal Hash(SerializationInfo info, StreamingContext context)
        {
            //
            // We have three serialization formats that we might be deserializing, the Whidbey format which
            // contains hash values directly, the Whidbey format which contains a pointer to a PEImage, and
            // the v4 format which contains a dictionary of calculated hashes.
            // 
            // If we have the Whidbey version that has built in hash values, we can convert that, but we
            // cannot do anything with the PEImage format since that is a serialized pointer into another
            // runtime's VM.
            // 

            Dictionary<Type, byte[]> hashes = info.GetValueNoThrow("Hashes", typeof(Dictionary<Type, byte[]>)) as Dictionary<Type, byte[]>;
            if (hashes != null)
            {
                m_hashes = hashes;
            }
            else
            {
                // If there is no hash value dictionary, then check to see if we have the Whidbey multiple
                // hashes version of the evidence.
                m_hashes = new Dictionary<Type, byte[]>();

                byte[] md5 = info.GetValueNoThrow("Md5", typeof(byte[])) as byte[];
                if (md5 != null)
                {
                    m_hashes[typeof(MD5)] = md5;
                }

                byte[] sha1 = info.GetValueNoThrow("Sha1", typeof(byte[])) as byte[];
                if (sha1 != null)
                {
                    m_hashes[typeof(SHA1)] = sha1;
                }

                byte[] rawData = info.GetValueNoThrow("RawData", typeof(byte[])) as byte[];
                if (rawData != null)
                {
                    GenerateDefaultHashes(rawData);
                }
            }
        }

        /// <summary>
        ///     Create hash evidence for the specified assembly
        /// </summary>
        public Hash(Assembly assembly)
        {
            if (assembly == null)
                throw new ArgumentNullException("assembly");
            Contract.EndContractBlock();
            if (assembly.IsDynamic)
                throw new ArgumentException(Environment.GetResourceString("Security_CannotGenerateHash"), "assembly");

            m_hashes = new Dictionary<Type, byte[]>();
            m_assembly = assembly as RuntimeAssembly;

            if (m_assembly == null)
                throw new ArgumentException(Environment.GetResourceString("Argument_MustBeRuntimeAssembly"), "assembly");
        }

        /// <summary>
        ///     Create a copy of some hash evidence
        /// </summary>
        private Hash(Hash hash)
        {
            Contract.Assert(hash != null);

            m_assembly = hash.m_assembly;
            m_rawData = hash.m_rawData;
            m_hashes = new Dictionary<Type, byte[]>(hash.m_hashes);
        }

        /// <summary>
        ///     Create a hash evidence prepopulated with a specific hash value
        /// </summary>
        private Hash(Type hashType, byte[] hashValue)
        {
            Contract.Assert(hashType != null);
            Contract.Assert(hashValue != null);

            m_hashes = new Dictionary<Type, byte[]>();

            byte[] hashClone = new byte[hashValue.Length];
            Array.Copy(hashValue, hashClone, hashClone.Length);

            m_hashes[hashType] = hashValue;
        }

        /// <summary>
        ///     Build a Hash evidence that contains the specific SHA-1 hash.  The input hash is not validated,
        ///     and the resulting Hash object cannot calculate additional hash values.
        /// </summary>
        public static Hash CreateSHA1(byte[] sha1)
        {
            if (sha1 == null)
                throw new ArgumentNullException("sha1");
            Contract.EndContractBlock();

            return new Hash(typeof(SHA1), sha1);
        }

        /// <summary>
        ///     Build a Hash evidence that contains the specific SHA-256 hash.  The input hash is not
        ///     validated, and the resulting Hash object cannot calculate additional hash values.
        /// </summary>
        public static Hash CreateSHA256(byte[] sha256)
        {
            if (sha256 == null)
                throw new ArgumentNullException("sha256");
            Contract.EndContractBlock();

            return new Hash(typeof(SHA256), sha256);
        }

        /// <summary>
        ///     Build a Hash evidence that contains the specific MD5 hash.  The input hash is not validated,
        ///     and the resulting Hash object cannot calculate additional hash values.
        /// </summary>
        public static Hash CreateMD5(byte[] md5)
        {
            if (md5 == null)
                throw new ArgumentNullException("md5");
            Contract.EndContractBlock();

            return new Hash(typeof(MD5), md5);
        }

        /// <summary>
        ///     Make a copy of this evidence object
        /// </summary>
        public override EvidenceBase Clone()
        {
            return new Hash(this);
        }

        /// <summary>
        ///     Prepare the hash evidence for serialization
        /// </summary>
        [OnSerializing]
        private void OnSerializing(StreamingContext ctx)
        {
            GenerateDefaultHashes();
        }

        /// <summary>
        ///     Serialize the hash evidence
        /// </summary>
        [SecurityCritical]
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            GenerateDefaultHashes();

            //
            // Backwards compatibility with Whidbey
            //

            byte[] sha1Hash;
            byte[] md5Hash;

            // Whidbey expects the MD5 and SHA1 hashes stored separately.
            if (m_hashes.TryGetValue(typeof(MD5), out md5Hash))
            {
                info.AddValue("Md5", md5Hash);
            }
            if (m_hashes.TryGetValue(typeof(SHA1), out sha1Hash))
            {
                info.AddValue("Sha1", sha1Hash);
            }
            
            // For perf, don't serialize the assembly binary content.
            // This has the side-effect that the Whidbey runtime will not be able to compute any 
            // hashes besides the provided MD5 and SHA1.
            info.AddValue("RawData", null);
            // It doesn't make sense to serialize a memory pointer cross-runtime.
            info.AddValue("PEFile", IntPtr.Zero);

            //
            // Current implementation
            //

            // Add all the computed hashes. While this can duplicate the MD5 and SHA1 hashes, 
            // it allows for a clean separation between legacy support and the current implementation.
            info.AddValue("Hashes", m_hashes);
        }

        /// <summary>
        ///     Get the SHA-1 hash value of the assembly
        /// </summary>
        public byte[] SHA1
        {
            get
            {
                byte[] sha1 = null;
                if (!m_hashes.TryGetValue(typeof(SHA1), out sha1))
                {
                    sha1 = GenerateHash(GetDefaultHashImplementationOrFallback(typeof(SHA1), typeof(SHA1)));
                }

                byte[] returnHash = new byte[sha1.Length];
                Array.Copy(sha1, returnHash, returnHash.Length);
                return returnHash;
            }
        }

        /// <summary>
        ///     Get the SHA-256 hash value of the assembly
        /// </summary>
        public byte[] SHA256
        {
            get
            {
                byte[] sha256 = null;
                if (!m_hashes.TryGetValue(typeof(SHA256), out sha256))
                {
                    sha256 = GenerateHash(GetDefaultHashImplementationOrFallback(typeof(SHA256), typeof(SHA256)));
                }

                byte[] returnHash = new byte[sha256.Length];
                Array.Copy(sha256, returnHash, returnHash.Length);
                return returnHash;
            }
        }

        /// <summary>
        ///     Get the MD5 hash value of the assembly
        /// </summary>
        public byte[] MD5
        {
            get
            {
                byte[] md5 = null;
                if (!m_hashes.TryGetValue(typeof(MD5), out md5))
                {
                    md5 = GenerateHash(GetDefaultHashImplementationOrFallback(typeof(MD5), typeof(MD5)));
                }

                byte[] returnHash = new byte[md5.Length];
                Array.Copy(md5, returnHash, returnHash.Length);
                return returnHash;
            }
        }

        /// <summary>
        ///     Get the hash value of the assembly when hashed with a specific algorithm.  The actual hash
        ///     algorithm object is not used, however the same type of object will be used.
        /// </summary>
        public byte[] GenerateHash(HashAlgorithm hashAlg)
        {
            if (hashAlg == null)
                throw new ArgumentNullException("hashAlg");
            Contract.EndContractBlock();

            byte[] hashValue = GenerateHash(hashAlg.GetType());

            byte[] returnHash = new byte[hashValue.Length];
            Array.Copy(hashValue, returnHash, returnHash.Length);
            return returnHash;
        }

        /// <summary>
        ///     Generate the hash value of an assembly when hashed with the specified algorithm. The result
        ///     may be a direct reference to our internal table of hashes, so it should be copied before
        ///     returning it to user code.
        /// </summary>
        private byte[] GenerateHash(Type hashType)
        {
            Contract.Assert(hashType != null && typeof(HashAlgorithm).IsAssignableFrom(hashType), "Expected a hash algorithm");

            Type indexType = GetHashIndexType(hashType);
            byte[] hashValue = null;
            if (!m_hashes.TryGetValue(indexType, out hashValue))
            {
                // If we're not attached to an assembly, then we cannot generate hashes on demand
                if (m_assembly == null)
                {
                    throw new InvalidOperationException(Environment.GetResourceString("Security_CannotGenerateHash"));
                }

                hashValue = GenerateHash(hashType, GetRawData());
                m_hashes[indexType] = hashValue;
            }

            return hashValue;
        }

        /// <summary>
        ///     Generate a hash of the given type for the assembly data
        /// </summary>
        private static byte[] GenerateHash(Type hashType, byte[] assemblyBytes)
        {
            Contract.Assert(hashType != null && typeof(HashAlgorithm).IsAssignableFrom(hashType), "Expected a hash algorithm");
            Contract.Assert(assemblyBytes != null);

            using (HashAlgorithm hash = HashAlgorithm.Create(hashType.FullName))
            {
                return hash.ComputeHash(assemblyBytes);
            }
        }


        /// <summary>
        ///     Build the default set of hash values that will be available for all assemblies
        /// </summary>
        private void GenerateDefaultHashes()
        {
            // We can't generate any hash values that we don't already have if there isn't an attached
            // assembly to get the hash value of.
            if (m_assembly != null)
            {
                GenerateDefaultHashes(GetRawData());
            }
        }

        /// <summary>
        ///     Build the default set of hash values that will be available for all assemblies given the raw
        ///     assembly data to hash.
        /// </summary>
        private void GenerateDefaultHashes(byte[] assemblyBytes)
        {
            Contract.Assert(assemblyBytes != null);

            Type[] defaultHashTypes = new Type[]
            {
                GetHashIndexType(typeof(SHA1)),
                GetHashIndexType(typeof(SHA256)),
                GetHashIndexType(typeof(MD5))
            };

            foreach (Type defaultHashType in defaultHashTypes)
            {
                Type hashImplementationType = GetDefaultHashImplementation(defaultHashType);
                if (hashImplementationType != null)
                {
                    if (!m_hashes.ContainsKey(defaultHashType))
                    {
                        m_hashes[defaultHashType] = GenerateHash(hashImplementationType, assemblyBytes);
                    }
                }
            }
        }

        /// <summary>
        ///     Map a hash algorithm to the default implementation of that algorithm, falling back to a given
        ///     implementation if no suitable default can be found.  This option may be used for situations
        ///     where it is better to throw an informative exception when trying to use an unsuitable hash
        ///     algorithm than to just return null.  (For instance, throwing a FIPS not supported exception
        ///     when trying to get the MD5 hash evidence).
        /// </summary>
        private static Type GetDefaultHashImplementationOrFallback(Type hashAlgorithm,
                                                                   Type fallbackImplementation)
        {
            Contract.Assert(hashAlgorithm != null && typeof(HashAlgorithm).IsAssignableFrom(hashAlgorithm));
            Contract.Assert(fallbackImplementation != null && GetHashIndexType(hashAlgorithm).IsAssignableFrom(fallbackImplementation));

            Type defaultImplementation = GetDefaultHashImplementation(hashAlgorithm);
            return defaultImplementation != null ? defaultImplementation : fallbackImplementation;
        }

        /// <summary>
        ///     Map a hash algorithm to the default implementation of that algorithm to use for Hash
        ///     evidence, taking into account things such as FIPS support.  If there is no suitable
        ///     implementation for the algorithm, GetDefaultHashImplementation returns null.
        /// </summary>
        private static Type GetDefaultHashImplementation(Type hashAlgorithm)
        {
            Contract.Assert(hashAlgorithm != null && typeof(HashAlgorithm).IsAssignableFrom(hashAlgorithm));

            if (hashAlgorithm.IsAssignableFrom(typeof(MD5)))
            {
                // MD5 is not a FIPS compliant algorithm, so if we need to allow only FIPS implementations,
                // we have no way to create an MD5 hash.  Otherwise, we can just use the standard CAPI
                // implementation since that is available on all operating systems we support.
                if (!CryptoConfig.AllowOnlyFipsAlgorithms)
                {
                    return typeof(MD5CryptoServiceProvider);
                }
                else
                {
                    return null;
                }
            }
            else if (hashAlgorithm.IsAssignableFrom(typeof(SHA256)))
            {
                // The managed SHA256 implementation is not a FIPS certified implementation, however on
                // we have a FIPS alternative.
                return Type.GetType("System.Security.Cryptography.SHA256CryptoServiceProvider, " + AssemblyRef.SystemCore);
            }
            else
            {
                // Otherwise we don't have a better suggestion for the algorithm, so we can just fallback to
                // the input algorithm.
                return hashAlgorithm;
            }
        }

        /// <summary>
        ///     Get the type used to index into the saved hash value dictionary.  We want this to be the
        ///     class which immediately derives from HashAlgorithm so that we can reuse the same hash value
        ///     if we're asked for (e.g.) SHA256Managed and SHA256CryptoServiceProvider
        /// </summary>
        private static Type GetHashIndexType(Type hashType)
        {
            Contract.Assert(hashType != null && typeof(HashAlgorithm).IsAssignableFrom(hashType));

            Type currentType = hashType;

            // Walk up the inheritence hierarchy looking for the first class that derives from HashAlgorithm
            while (currentType != null && currentType.BaseType != typeof(HashAlgorithm))
            {
                currentType = currentType.BaseType;
            }

            // If this is the degenerate case where we started out with HashAlgorithm, we won't find it
            // further up our inheritence tree.
            if (currentType == null)
            {
                BCLDebug.Assert(hashType == typeof(HashAlgorithm), "hashType == typeof(HashAlgorithm)");
                currentType = typeof(HashAlgorithm);
            }

            return currentType;
        }

        /// <summary>
        ///     Raw bytes of the assembly being hashed
        /// </summary>
        private byte[] GetRawData()
        {
            byte[] rawData = null;

            // We can only generate hashes on demand if we're associated with an assembly
            if (m_assembly != null)
            {
                // See if we still hold a reference to the assembly data
                if (m_rawData != null)
                {
                    rawData = m_rawData.Target as byte[];
                }

                // If not, load the raw bytes up
                if (rawData == null)
                {
                    rawData = m_assembly.GetRawBytes();
                    m_rawData = new WeakReference(rawData);
                }
            }

            return rawData;
        }

        private SecurityElement ToXml()
        {
            GenerateDefaultHashes();

            SecurityElement root = new SecurityElement("System.Security.Policy.Hash");
            // If you hit this assert then most likely you are trying to change the name of this class. 
            // This is ok as long as you change the hard coded string above and change the assert below.
            BCLDebug.Assert(this.GetType().FullName.Equals("System.Security.Policy.Hash"), "Class name changed!");

            root.AddAttribute("version", "2");
            foreach (KeyValuePair<Type, byte[]> hashValue in m_hashes)
            {
                SecurityElement hashElement = new SecurityElement("hash");
                hashElement.AddAttribute("algorithm", hashValue.Key.Name);
                hashElement.AddAttribute("value", Hex.EncodeHexString(hashValue.Value));

                root.AddChild(hashElement);
            }

            return root;
        }

        public override String ToString()
        {
            return ToXml().ToString();
        }
    }
}
