using GostCryptography.Asn1.Gost.Gost_28147_89;
using GostCryptography.Asn1.Gost.Gost_R3410;
using GostCryptography.Base;
using GostCryptography.Properties;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;

namespace GostCryptography.Native
{
    /// <summary>
    /// Вспомогательные методы для работы с Microsoft CryptoAPI
    /// </summary>
    [SecurityCritical]
    public static class CryptoApiHelper
    {
        /// <summary>
        /// Возвращает <see langword="true"/>, если заданный провайдер установлен
        /// </summary>
        [SecurityCritical]
        public static bool IsInstalled(ProviderType providerType)
        {
            try
            {
                var providerHandle = GetProviderHandle(providerType);
                return !providerHandle.IsInvalid;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Возвращает доступный провайдер для ключей ГОСТ Р 34.10-2001
        /// </summary>
        /// <exception cref="CryptographicException">Провайдер не установлен</exception>
        [SecuritySafeCritical]
        public static ProviderType GetAvailableProviderType_2001()
        {
            if (IsInstalled(ProviderType.VipNet))
            {
                return ProviderType.VipNet;
            }

            if (IsInstalled(ProviderType.CryptoPro))
            {
                return ProviderType.CryptoPro;
            }

            throw ExceptionUtility.CryptographicException(Resources.Provider_2001_IsNotInstalled);
        }

        /// <summary>
        /// Возвращает доступный провайдер для ключей ГОСТ Р 34.10-2012/512
        /// </summary>
        /// <exception cref="CryptographicException">Провайдер не установлен</exception>
        [SecuritySafeCritical]
        public static ProviderType GetAvailableProviderType_2012_512()
        {
            if (IsInstalled(ProviderType.VipNet_2012_512))
            {
                return ProviderType.VipNet_2012_512;
            }

            if (IsInstalled(ProviderType.CryptoPro_2012_512))
            {
                return ProviderType.CryptoPro_2012_512;
            }

            throw ExceptionUtility.CryptographicException(Resources.Provider_2012_512_IsNotInstalled);
        }

        /// <summary>
        /// Возвращает доступный провайдер для ключей ГОСТ Р 34.10-2012/1024
        /// </summary>
        /// <exception cref="CryptographicException">Провайдер не установлен</exception>
        [SecuritySafeCritical]
        public static ProviderType GetAvailableProviderType_2012_1024()
        {
            if (IsInstalled(ProviderType.VipNet_2012_1024))
            {
                return ProviderType.VipNet_2012_1024;
            }

            if (IsInstalled(ProviderType.CryptoPro_2012_1024))
            {
                return ProviderType.CryptoPro_2012_1024;
            }

            throw ExceptionUtility.CryptographicException(Resources.Provider_2012_1024_IsNotInstalled);
        }

        #region Общие объекты

        private static readonly object ProviderHandleSync = new object();
        private static volatile Dictionary<ProviderType, SafeProvHandleImpl> _providerHandles = new Dictionary<ProviderType, SafeProvHandleImpl>();

        public static SafeProvHandleImpl GetProviderHandle(ProviderType providerType)
        {
            if (!_providerHandles.ContainsKey(providerType))
            {
                lock (ProviderHandleSync)
                {
                    if (!_providerHandles.ContainsKey(providerType))
                    {
                        var providerParams = new CspParameters(providerType.ToInt());
                        var providerHandle = AcquireProvider(providerParams);

                        Thread.MemoryBarrier();

                        _providerHandles.Add(providerType, providerHandle);
                    }
                }
            }

            return _providerHandles[providerType];
        }

        private static readonly object RandomNumberGeneratorSync = new object();
        private static volatile Dictionary<ProviderType, RNGCryptoServiceProvider> _randomNumberGenerators = new Dictionary<ProviderType, RNGCryptoServiceProvider>();

        public static RNGCryptoServiceProvider GetRandomNumberGenerator(ProviderType providerType)
        {
            if (!_randomNumberGenerators.ContainsKey(providerType))
            {
                lock (RandomNumberGeneratorSync)
                {
                    if (!_randomNumberGenerators.ContainsKey(providerType))
                    {
                        var providerParams = new CspParameters(providerType.ToInt());
                        var randomNumberGenerator = new RNGCryptoServiceProvider(providerParams);

                        Thread.MemoryBarrier();

                        _randomNumberGenerators.Add(providerType, randomNumberGenerator);
                    }
                }
            }

            return _randomNumberGenerators[providerType];
        }

        #endregion

        #region Для работы с криптографическим провайдером

        public static SafeProvHandleImpl AcquireProvider(CspParameters providerParameters)
        {
            var providerHandle = SafeProvHandleImpl.InvalidHandle;

            var dwFlags = Constants.CRYPT_VERIFYCONTEXT;

            if ((providerParameters.Flags & CspProviderFlags.UseMachineKeyStore) != CspProviderFlags.NoFlags)
            {
                dwFlags |= Constants.CRYPT_MACHINE_KEYSET;
            }

            if (!CryptoApi.CryptAcquireContext(ref providerHandle, providerParameters.KeyContainerName, providerParameters.ProviderName, (uint)providerParameters.ProviderType, dwFlags))
            {
                throw CreateWin32Error();
            }

            return providerHandle;
        }

        public static SafeProvHandleImpl OpenProvider(CspParameters providerParameters)
        {
            var providerHandle = SafeProvHandleImpl.InvalidHandle;
            var dwFlags = MapCspProviderFlags(providerParameters.Flags);

            if (!CryptoApi.CryptAcquireContext(ref providerHandle, providerParameters.KeyContainerName, providerParameters.ProviderName, (uint)providerParameters.ProviderType, dwFlags))
            {
                throw CreateWin32Error();
            }

            return providerHandle;
        }

        public static SafeProvHandleImpl CreateProvider(CspParameters providerParameters)
        {
            var providerHandle = SafeProvHandleImpl.InvalidHandle;

            if (!CryptoApi.CryptAcquireContext(ref providerHandle, providerParameters.KeyContainerName, providerParameters.ProviderName, (uint)providerParameters.ProviderType, Constants.CRYPT_NEWKEYSET))
            {
                throw CreateWin32Error();
            }

            return providerHandle;
        }

        private static uint MapCspProviderFlags(CspProviderFlags flags)
        {
            uint dwFlags = 0;

            if ((flags & CspProviderFlags.UseMachineKeyStore) != CspProviderFlags.NoFlags)
            {
                dwFlags |= Constants.CRYPT_MACHINE_KEYSET;
            }

            if ((flags & CspProviderFlags.NoPrompt) != CspProviderFlags.NoFlags)
            {
                dwFlags |= Constants.CRYPT_PREGEN;
            }

            return dwFlags;
        }

        public static void SetProviderParameter(SafeProvHandleImpl providerHandle, int keyNumber, uint keyParamId, IntPtr keyParamValue)
        {
            if ((keyParamId == Constants.PP_KEYEXCHANGE_PIN) || (keyParamId == Constants.PP_SIGNATURE_PIN))
            {
                if (keyNumber == Constants.AT_KEYEXCHANGE)
                {
                    keyParamId = Constants.PP_KEYEXCHANGE_PIN;
                }
                else if (keyNumber == Constants.AT_SIGNATURE)
                {
                    keyParamId = Constants.PP_SIGNATURE_PIN;
                }
                else
                {
                    throw ExceptionUtility.NotSupported(Resources.KeyAlgorithmNotSupported);
                }
            }

            if (!CryptoApi.CryptSetProvParam(providerHandle, keyParamId, keyParamValue, 0))
            {
                throw CreateWin32Error();
            }
        }

        public static ProviderType GetProviderType(SafeProvHandleImpl providerHandle)
        {
            uint providerTypeLen = sizeof(uint);
            byte[] dwData = new byte[sizeof(uint)];

            if (!CryptoApi.CryptGetProvParam(providerHandle, Constants.PP_PROVTYPE, dwData, ref providerTypeLen, 0))
            {
                throw CreateWin32Error();
            }

            var providerType = BitConverter.ToUInt32(dwData, 0);

            return (ProviderType)providerType;
        }

        /// <summary>
        /// Возвращает список имен контейнеров указанного криптопровайдера
        /// </summary>
        /// <param name="providerType">Тип криптопровайдера</param>
        /// <param name="fullContainerName">Вернуть полное имя контейнера</param>
        /// <param name="storeLocation">Область поиска контейнеров</param>
        public static IEnumerable<string> GetContainers(ProviderType providerType, bool fullContainerName = true, StoreLocation storeLocation = StoreLocation.LocalMachine)
        {
            var containers = new List<string>();

            var providerParameters = new CspParameters((int)providerType);

            if (storeLocation == StoreLocation.LocalMachine)
            {
                providerParameters.Flags |= CspProviderFlags.UseMachineKeyStore;
            }

            using (var providerHandle = AcquireProvider(providerParameters))
            {
                var containerNameMaxLength = 0U;
                var flag = Constants.CRYPT_FIRST;

                if (fullContainerName)
                {
                    flag |= (providerType.IsVipNet() ? Constants.CRYPT_UNIQUE : Constants.CRYPT_FQCN);
                }

                if (!CryptoApi.CryptGetProvParam(providerHandle, Constants.PP_ENUMCONTAINERS, (StringBuilder)null, ref containerNameMaxLength, flag))
                {
                    if (Marshal.GetLastWin32Error() != Constants.ERROR_NO_MORE_ITEMS)
                    {
                        throw CreateWin32Error();
                    }

                    return containers;
                }

                while (true)
                {
                    var containerName = new StringBuilder((int)containerNameMaxLength);

                    if (!CryptoApi.CryptGetProvParam(providerHandle, Constants.PP_ENUMCONTAINERS, containerName, ref containerNameMaxLength, flag))
                    {
                        if (Marshal.GetLastWin32Error() != Constants.ERROR_NO_MORE_ITEMS)
                        {
                            throw CreateWin32Error();
                        }

                        break;
                    }

                    containers.Add(containerName.ToString());

                    flag = Constants.CRYPT_NEXT;

                    if (fullContainerName)
                    {
                        flag |= (providerType.IsVipNet() ? Constants.CRYPT_UNIQUE : Constants.CRYPT_FQCN);
                    }
                }
            }

            return containers;
        }

        /// <summary>
        /// Возвращает сертификат X.509 для указанного ключа
        /// </summary>
        /// <param name="keyHandle">Дескриптор ключа сертификата</param>
        public static X509Certificate2 GetKeyCertificate(SafeKeyHandleImpl keyHandle)
        {
            uint certDataLength = 0;

            if (!CryptoApi.CryptGetKeyParam(keyHandle, Constants.KP_CERTIFICATE, null, ref certDataLength, 0))
            {
                if (Marshal.GetLastWin32Error() != Constants.ERROR_NO_SUCH_CERTIFICATE)
                {
                    throw CreateWin32Error();
                }

                return null;
            }

            var certData = new byte[certDataLength];

            if (!CryptoApi.CryptGetKeyParam(keyHandle, Constants.KP_CERTIFICATE, certData, ref certDataLength, 0))
            {
                throw CreateWin32Error();
            }

            return new X509Certificate2(certData);
        }

        #endregion

        #region Для работы с функцией хэширования криптографического провайдера

        public static SafeHashHandleImpl CreateHash_3411_94(SafeProvHandleImpl providerHandle)
        {
            return CreateHash_3411(providerHandle, Constants.CALG_GR3411);
        }

        public static SafeHashHandleImpl CreateHash_3411_2012_256(SafeProvHandleImpl providerHandle)
        {
            return CreateHash_3411(providerHandle, Constants.CALG_GR3411_2012_256);
        }

        public static SafeHashHandleImpl CreateHash_3411_2012_512(SafeProvHandleImpl providerHandle)
        {
            return CreateHash_3411(providerHandle, Constants.CALG_GR3411_2012_512);
        }

        private static SafeHashHandleImpl CreateHash_3411(SafeProvHandleImpl providerHandle, int hashAlgId)
        {
            var hashHandle = SafeHashHandleImpl.InvalidHandle;

            if (!CryptoApi.CryptCreateHash(providerHandle, (uint)hashAlgId, SafeKeyHandleImpl.InvalidHandle, 0, ref hashHandle))
            {
                throw CreateWin32Error();
            }

            return hashHandle;
        }

        public static SafeHashHandleImpl CreateHashImit(SafeProvHandleImpl providerHandle, SafeKeyHandleImpl symKeyHandle)
        {
            var hashImitHandle = SafeHashHandleImpl.InvalidHandle;

            if (!CryptoApi.CryptCreateHash(providerHandle, Constants.CALG_G28147_IMIT, symKeyHandle, 0, ref hashImitHandle))
            {
                throw CreateWin32Error();
            }

            return hashImitHandle;
        }

        public static SafeHashHandleImpl CreateHashHMAC_94(ProviderType providerType, SafeProvHandleImpl providerHandle, SafeKeyHandleImpl symKeyHandle)
        {
            var hmacAlgId = providerType.IsVipNet() ? Constants.CALG_GR3411 : Constants.CALG_GR3411_HMAC;
            return CreateHashHMAC(providerHandle, symKeyHandle, hmacAlgId);
        }

        public static SafeHashHandleImpl CreateHashHMAC_2012_256(ProviderType providerType, SafeProvHandleImpl providerHandle, SafeKeyHandleImpl symKeyHandle)
        {
            var hmacAlgId = providerType.IsVipNet() ? Constants.CALG_GR3411_2012_256 : Constants.CALG_GR3411_2012_256_HMAC;
            return CreateHashHMAC(providerHandle, symKeyHandle, hmacAlgId);
        }

        public static SafeHashHandleImpl CreateHashHMAC_2012_512(ProviderType providerType, SafeProvHandleImpl providerHandle, SafeKeyHandleImpl symKeyHandle)
        {
            var hmacAlgId = providerType.IsVipNet() ? Constants.CALG_GR3411_2012_512 : Constants.CALG_GR3411_2012_512_HMAC;
            return CreateHashHMAC(providerHandle, symKeyHandle, hmacAlgId);
        }

        private static SafeHashHandleImpl CreateHashHMAC(SafeProvHandleImpl providerHandle, SafeKeyHandleImpl symKeyHandle, int hmacAlgId)
        {
            var hashHmacHandle = SafeHashHandleImpl.InvalidHandle;

            if (!CryptoApi.CryptCreateHash(providerHandle, (uint)hmacAlgId, symKeyHandle, 0, ref hashHmacHandle))
            {
                var errorCode = Marshal.GetLastWin32Error();

                if (errorCode == Constants.NTE_BAD_ALGID)
                {
                    throw ExceptionUtility.CryptographicException(Resources.AlgorithmNotAvailable);
                }

                throw ExceptionUtility.CryptographicException(errorCode);
            }

            return hashHmacHandle;
        }

        public static unsafe void HashData(SafeHashHandleImpl hashHandle, byte[] data, int dataOffset, int dataLength)
        {
            if (data == null)
            {
                throw ExceptionUtility.ArgumentNull(nameof(data));
            }

            if (dataOffset < 0)
            {
                throw ExceptionUtility.ArgumentOutOfRange(nameof(dataOffset));
            }

            if (dataLength < 0)
            {
                throw ExceptionUtility.ArgumentOutOfRange(nameof(dataLength));
            }

            if (data.Length < dataOffset + dataLength)
            {
                throw ExceptionUtility.ArgumentOutOfRange(nameof(dataLength));
            }

            if (dataLength > 0)
            {
                fixed (byte* dataRef = data)
                {
                    var dataOffsetRef = dataRef + dataOffset;

                    if (!CryptoApi.CryptHashData(hashHandle, dataOffsetRef, (uint)dataLength, 0))
                    {
                        throw CreateWin32Error();
                    }
                }
            }
        }

        public static byte[] EndHashData(SafeHashHandleImpl hashHandle)
        {
            uint dataLength = 0;

            if (!CryptoApi.CryptGetHashParam(hashHandle, Constants.HP_HASHVAL, null, ref dataLength, 0))
            {
                throw CreateWin32Error();
            }

            var data = new byte[dataLength];

            if (!CryptoApi.CryptGetHashParam(hashHandle, Constants.HP_HASHVAL, data, ref dataLength, 0))
            {
                throw CreateWin32Error();
            }

            return data;
        }

        public static void HashKeyExchange(SafeHashHandleImpl hashHandle, SafeKeyHandleImpl keyExchangeHandle)
        {
            if (!CryptoApi.CryptHashSessionKey(hashHandle, keyExchangeHandle, 0))
            {
                throw CreateWin32Error();
            }
        }

        #endregion

        #region Для работы с функцией шифрования криптографического провайдера

        public static int EncryptData(ProviderType providerType, SafeKeyHandleImpl symKeyHandle, byte[] data, int dataOffset, int dataLength, ref byte[] encryptedData, int encryptedDataOffset, PaddingMode paddingMode, bool isDone, bool isStream)
        {
            if (dataOffset < 0)
            {
                throw ExceptionUtility.ArgumentOutOfRange(nameof(dataOffset));
            }

            if (dataLength < 0)
            {
                throw ExceptionUtility.ArgumentOutOfRange(nameof(dataLength));
            }

            if (dataOffset > data.Length)
            {
                throw ExceptionUtility.ArgumentOutOfRange(nameof(dataOffset), Resources.InvalidDataOffset);
            }

            var length = dataLength;

            if (isDone)
            {
                length += 8;
            }

            // Выровненные данные
            var dataAlignLength = (uint)dataLength;
            var dataAlignArray = new byte[length];
            Array.Clear(dataAlignArray, 0, length);
            Array.Copy(data, dataOffset, dataAlignArray, 0, dataLength);

            if (isDone)
            {
                var dataPadding = dataLength & 7;
                var dataPaddingSize = (byte)(8 - dataPadding);

                // Добавление дополнения данных в зависимости от настроек
                switch (paddingMode)
                {
                    case PaddingMode.None:
                        if ((dataPadding != 0) && !isStream)
                        {
                            throw ExceptionUtility.CryptographicException(Resources.EncryptInvalidDataSize);
                        }

                        break;
                    case PaddingMode.Zeros:
                        if (dataPadding != 0)
                        {
                            dataAlignLength += dataPaddingSize;

                            // Дополнение заполняется нулевыми байтами
                        }

                        break;
                    case PaddingMode.PKCS7:
                        {
                            dataAlignLength += dataPaddingSize;

                            var paddingIndex = dataLength;

                            // Дополнение заполняется байтами, в каждый из которых записывается размер дополнения
                            while (paddingIndex < dataAlignLength)
                            {
                                dataAlignArray[paddingIndex++] = dataPaddingSize;
                            }
                        }
                        break;
                    case PaddingMode.ANSIX923:
                        {
                            dataAlignLength += dataPaddingSize;

                            // Дополнение заполняется нулевыми, кроме последнего - в него записывается размер дополнения
                            dataAlignArray[(int)((IntPtr)(dataAlignLength - 1))] = dataPaddingSize;
                        }
                        break;
                    case PaddingMode.ISO10126:
                        {
                            dataAlignLength += dataPaddingSize;

                            // Дополнение заполняется случайными байтами, кроме последнего - в него записывается размер дополнения
                            var randomPadding = new byte[dataPaddingSize - 1];
                            GetRandomNumberGenerator(providerType).GetBytes(randomPadding);
                            randomPadding.CopyTo(dataAlignArray, dataLength);
                            dataAlignArray[(int)((IntPtr)(dataAlignLength - 1))] = dataPaddingSize;
                        }
                        break;
                    default:
                        throw ExceptionUtility.Argument(nameof(paddingMode), Resources.InvalidPaddingMode);
                }
            }

            // Шифрование данных
            if (!CryptoApi.CryptEncrypt(symKeyHandle, SafeHashHandleImpl.InvalidHandle, false, 0, dataAlignArray, ref dataAlignLength, (uint)length))
            {
                throw CreateWin32Error();
            }

            // Копирование результата шифрования данных
            if (encryptedData == null)
            {
                encryptedData = new byte[dataAlignLength];

                Array.Copy(dataAlignArray, 0L, encryptedData, 0L, dataAlignLength);
            }
            else
            {
                if (encryptedDataOffset < 0)
                {
                    throw ExceptionUtility.ArgumentOutOfRange(nameof(encryptedDataOffset));
                }

                if ((encryptedData.Length < dataAlignLength) || ((encryptedData.Length - dataAlignLength) < encryptedDataOffset))
                {
                    throw ExceptionUtility.ArgumentOutOfRange(nameof(encryptedDataOffset), Resources.InvalidDataOffset);
                }

                Array.Copy(dataAlignArray, 0L, encryptedData, encryptedDataOffset, dataAlignLength);
            }

            return (int)dataAlignLength;
        }

        public static int DecryptData(SafeKeyHandleImpl symKeyHandle, byte[] data, int dataOffset, int dataLength, ref byte[] decryptedData, int decryptedDataOffset, PaddingMode paddingMode, bool isDone)
        {
            if (dataOffset < 0)
            {
                throw ExceptionUtility.ArgumentOutOfRange(nameof(dataOffset));
            }

            if (dataLength < 0)
            {
                throw ExceptionUtility.ArgumentOutOfRange(nameof(dataLength));
            }

            if ((dataOffset > data.Length) || ((dataOffset + dataLength) > data.Length))
            {
                throw ExceptionUtility.ArgumentOutOfRange(nameof(dataOffset), Resources.InvalidDataOffset);
            }

            // Выровненные данные
            var dataAlignLength = (uint)dataLength;
            var dataAlign = new byte[dataAlignLength];
            Array.Copy(data, dataOffset, dataAlign, 0L, dataAlignLength);

            // Расшифровка данных
            if (!CryptoApi.CryptDecrypt(symKeyHandle, SafeHashHandleImpl.InvalidHandle, false, 0, dataAlign, ref dataAlignLength))
            {
                throw CreateWin32Error();
            }

            var length = (int)dataAlignLength;

            if (isDone)
            {
                byte dataPaddingSize = 0;

                // Удаление дополнения данных в зависимости от настроек
                if (((paddingMode == PaddingMode.PKCS7) || (paddingMode == PaddingMode.ANSIX923)) || (paddingMode == PaddingMode.ISO10126))
                {
                    if (dataAlignLength < 8)
                    {
                        throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_DATA);
                    }

                    // Размер дополнения находится в последнем байте
                    dataPaddingSize = dataAlign[(int)((IntPtr)(dataAlignLength - 1))];

                    if (dataPaddingSize > 8)
                    {
                        throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_DATA);
                    }

                    // Проверка корректности дополнения данных
                    if (paddingMode == PaddingMode.PKCS7)
                    {
                        for (var paddingIndex = dataAlignLength - dataPaddingSize; paddingIndex < (dataAlignLength - 1); paddingIndex++)
                        {
                            if (dataAlign[paddingIndex] != dataPaddingSize)
                            {
                                throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_DATA);
                            }
                        }
                    }
                    else if (paddingMode == PaddingMode.ANSIX923)
                    {
                        for (var paddingIndex = dataAlignLength - dataPaddingSize; paddingIndex < (dataAlignLength - 1); paddingIndex++)
                        {
                            if (dataAlign[paddingIndex] != 0)
                            {
                                throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_DATA);
                            }
                        }
                    }
                }
                else if ((paddingMode != PaddingMode.None) && (paddingMode != PaddingMode.Zeros))
                {
                    throw ExceptionUtility.Argument(nameof(paddingMode), Resources.InvalidPaddingMode);
                }

                length -= dataPaddingSize;
            }

            if (decryptedData == null)
            {
                decryptedData = new byte[length];

                Array.Copy(dataAlign, 0, decryptedData, 0, length);
            }
            else
            {
                if (decryptedDataOffset < 0)
                {
                    throw ExceptionUtility.ArgumentOutOfRange(nameof(decryptedDataOffset));
                }

                if ((decryptedData.Length < length) || ((decryptedData.Length - length) < decryptedDataOffset))
                {
                    throw ExceptionUtility.ArgumentOutOfRange(nameof(decryptedData), Resources.InvalidDataOffset);
                }

                Array.Copy(dataAlign, 0, decryptedData, decryptedDataOffset, length);
            }

            return length;
        }

        public static void EndEncrypt(ProviderType providerType, SafeKeyHandleImpl symKeyHandle)
        {
            uint dataLength = 0;
            var data = new byte[32];
            var success = CryptoApi.CryptEncrypt(symKeyHandle, SafeHashHandleImpl.InvalidHandle, true, 0, data, ref dataLength, (uint)data.Length);

            if (!success)
            {
                throw CreateWin32Error();
            }
        }

        public static void EndDecrypt(ProviderType providerType, SafeKeyHandleImpl symKeyHandle)
        {
            uint dataLength = 0;
            var data = new byte[0];
            var success = CryptoApi.CryptDecrypt(symKeyHandle, SafeHashHandleImpl.InvalidHandle, true, 0, data, ref dataLength) || providerType.IsVipNet();

            if (!success)
            {
                throw CreateWin32Error();
            }
        }

        #endregion

        #region Для работы с ключами криптографического провайдера

        public static SafeKeyHandleImpl GenerateKey(SafeProvHandleImpl providerHandle, int algId, CspProviderFlags flags)
        {
            var keyHandle = SafeKeyHandleImpl.InvalidHandle;
            var dwFlags = MapCspKeyFlags(flags);

            if (!CryptoApi.CryptGenKey(providerHandle, (uint)algId, dwFlags, ref keyHandle))
            {
                throw CreateWin32Error();
            }

            return keyHandle;
        }

        public static SafeKeyHandleImpl GenerateDhEphemeralKey(ProviderType providerType, SafeProvHandleImpl providerHandle, int algId, string digestParamSet, string publicKeyParamSet)
        {
            var keyHandle = SafeKeyHandleImpl.InvalidHandle;
            var dwFlags = MapCspKeyFlags(CspProviderFlags.NoFlags) | Constants.CRYPT_PREGEN;

            if (!CryptoApi.CryptGenKey(providerHandle, (uint)algId, dwFlags, ref keyHandle))
            {
                throw CreateWin32Error();
            }

            if (!providerType.IsVipNet())
            {
                SetKeyParameterString(keyHandle, Constants.KP_HASHOID, digestParamSet);
            }

            SetKeyParameterString(keyHandle, Constants.KP_DHOID, publicKeyParamSet);
            SetKeyParameter(keyHandle, Constants.KP_X, null);

            return keyHandle;
        }

        private static uint MapCspKeyFlags(CspProviderFlags flags)
        {
            uint dwFlags = 0;

            if ((flags & CspProviderFlags.UseNonExportableKey) == CspProviderFlags.NoFlags)
            {
                dwFlags |= Constants.CRYPT_EXPORTABLE;
            }

            if ((flags & CspProviderFlags.UseArchivableKey) != CspProviderFlags.NoFlags)
            {
                dwFlags |= Constants.CRYPT_ARCHIVABLE;
            }

            if ((flags & CspProviderFlags.UseUserProtectedKey) != CspProviderFlags.NoFlags)
            {
                dwFlags |= Constants.CRYPT_USER_PROTECTED;
            }

            return dwFlags;
        }

        public static SafeKeyHandleImpl GetUserKey(SafeProvHandleImpl providerHandle, int keyNumber)
        {
            var keyHandle = SafeKeyHandleImpl.InvalidHandle;

            if (!CryptoApi.CryptGetUserKey(providerHandle, (uint)keyNumber, ref keyHandle))
            {
                throw CreateWin32Error();
            }

            return keyHandle;
        }

        public static SafeKeyHandleImpl DeriveSymKey(SafeProvHandleImpl providerHandle, SafeHashHandleImpl hashHandle)
        {
            var symKeyHandle = SafeKeyHandleImpl.InvalidHandle;

            if (!CryptoApi.CryptDeriveKey(providerHandle, Constants.CALG_G28147, hashHandle, Constants.CRYPT_EXPORTABLE, ref symKeyHandle))
            {
                throw CreateWin32Error();
            }

            return symKeyHandle;
        }

        public static SafeKeyHandleImpl DuplicateKey(IntPtr sourceKeyHandle)
        {
            var keyHandle = SafeKeyHandleImpl.InvalidHandle;

            if (!CryptoApi.CryptDuplicateKey(sourceKeyHandle, null, 0, ref keyHandle))
            {
                throw CreateWin32Error();
            }

            return keyHandle;
        }

        public static SafeKeyHandleImpl DuplicateKey(SafeKeyHandleImpl sourceKeyHandle)
        {
            return DuplicateKey(sourceKeyHandle.DangerousGetHandle());
        }

        public static int GetKeyParameterInt32(SafeKeyHandleImpl keyHandle, uint keyParamId)
        {
            const int doubleWordSize = 4;

            uint dwDataLength = doubleWordSize;
            var dwDataBytes = new byte[doubleWordSize];

            if (!CryptoApi.CryptGetKeyParam(keyHandle, keyParamId, dwDataBytes, ref dwDataLength, 0))
            {
                throw CreateWin32Error();
            }

            if (dwDataLength != doubleWordSize)
            {
                throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_DATA);
            }

            return BitConverter.ToInt32(dwDataBytes, 0);
        }

        private static string GetKeyParameterString(SafeKeyHandleImpl keyHandle, uint keyParamId)
        {
            var paramValue = GetKeyParameter(keyHandle, keyParamId);

            return BytesToString(paramValue);
        }

        private static string BytesToString(byte[] value)
        {
            string valueString;

            try
            {
                valueString = Encoding.GetEncoding(0).GetString(value);

                var length = 0;

                while (length < valueString.Length)
                {
                    // Строка заканчивается нулевым символом
                    if (valueString[length] == '\0')
                    {
                        break;
                    }

                    length++;
                }

                if (length == valueString.Length)
                {
                    throw ExceptionUtility.CryptographicException(Resources.InvalidString);
                }

                valueString = valueString.Substring(0, length);
            }
            catch (DecoderFallbackException exception)
            {
                throw ExceptionUtility.CryptographicException(exception, Resources.InvalidString);
            }

            return valueString;
        }

        public static byte[] GetKeyParameter(SafeKeyHandleImpl keyHandle, uint keyParamId)
        {
            uint dataLength = 0;

            if (!CryptoApi.CryptGetKeyParam(keyHandle, keyParamId, null, ref dataLength, 0))
            {
                throw CreateWin32Error();
            }

            var dataBytes = new byte[dataLength];

            if (!CryptoApi.CryptGetKeyParam(keyHandle, keyParamId, dataBytes, ref dataLength, 0))
            {
                throw CreateWin32Error();
            }

            return dataBytes;
        }

        public static void SetKeyExchangeExportAlgId(ProviderType providerType, SafeKeyHandleImpl keyHandle, int keyExchangeExportAlgId)
        {
            var keyExchangeExportAlgParamId = providerType.IsVipNet() ? Constants.KP_EXPORTID : Constants.KP_ALGID;
            SetKeyParameterInt32(keyHandle, keyExchangeExportAlgParamId, keyExchangeExportAlgId);
        }

        public static void SetKeyParameterInt32(SafeKeyHandleImpl keyHandle, int keyParamId, int keyParamValue)
        {
            var dwDataBytes = BitConverter.GetBytes(keyParamValue);

            if (!CryptoApi.CryptSetKeyParam(keyHandle, (uint)keyParamId, dwDataBytes, 0))
            {
                throw CreateWin32Error();
            }
        }

        private static void SetKeyParameterString(SafeKeyHandleImpl keyHandle, int keyParamId, string keyParamValue)
        {
            var stringDataBytes = Encoding.GetEncoding(0).GetBytes(keyParamValue);

            if (!CryptoApi.CryptSetKeyParam(keyHandle, (uint)keyParamId, stringDataBytes, 0))
            {
                throw CreateWin32Error();
            }
        }

        public static void SetKeyParameter(SafeKeyHandleImpl keyHandle, int keyParamId, byte[] keyParamValue)
        {
            if (!CryptoApi.CryptSetKeyParam(keyHandle, (uint)keyParamId, keyParamValue, 0))
            {
                throw CreateWin32Error();
            }
        }

        #endregion

        #region Для экспорта ключей криптографического провайдера

        public static byte[] ExportCspBlob(SafeKeyHandleImpl symKeyHandle, SafeKeyHandleImpl keyExchangeHandle, int blobType)
        {
            uint exportedKeyLength = 0;

            if (!CryptoApi.CryptExportKey(symKeyHandle, keyExchangeHandle, (uint)blobType, 0, null, ref exportedKeyLength))
            {
                throw CreateWin32Error();
            }

            var exportedKeyBytes = new byte[exportedKeyLength];

            if (!CryptoApi.CryptExportKey(symKeyHandle, keyExchangeHandle, (uint)blobType, 0, exportedKeyBytes, ref exportedKeyLength))
            {
                throw CreateWin32Error();
            }

            return exportedKeyBytes;
        }

        public static T ExportPublicKey<T>(SafeKeyHandleImpl symKeyHandle, T keyExchangeParams, int keySize) where T : Gost_R3410_KeyExchangeParams
        {
            var exportedKeyBytes = ExportCspBlob(symKeyHandle, SafeKeyHandleImpl.InvalidHandle, Constants.PUBLICKEYBLOB);
            return DecodePublicBlob(exportedKeyBytes, keyExchangeParams, keySize);
        }

        private static T DecodePublicBlob<T>(byte[] encodedPublicBlob, T keyExchangeParams, int keySize) where T : Gost_R3410_KeyExchangeParams
        {
            if (encodedPublicBlob == null)
            {
                throw ExceptionUtility.ArgumentNull(nameof(encodedPublicBlob));
            }

            if (encodedPublicBlob.Length < 16 + keySize / 8)
            {
                throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_DATA);
            }

            var gostKeyMask = BitConverter.ToUInt32(encodedPublicBlob, 8);

            if (gostKeyMask != Constants.GR3410_1_MAGIC)
            {
                throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_DATA);
            }

            var gostKeySize = BitConverter.ToUInt32(encodedPublicBlob, 12);

            if (gostKeySize != keySize)
            {
                throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_DATA);
            }

            var encodeKeyParameters = new byte[encodedPublicBlob.Length - 16 - keySize / 8];
            Array.Copy(encodedPublicBlob, 16, encodeKeyParameters, 0, encodedPublicBlob.Length - 16 - keySize / 8);
            keyExchangeParams.DecodeParameters(encodeKeyParameters);

            var publicKey = new byte[keySize / 8];
            Array.Copy(encodedPublicBlob, encodedPublicBlob.Length - keySize / 8, publicKey, 0, keySize / 8);
            keyExchangeParams.PublicKey = publicKey;

            return keyExchangeParams;
        }

        public static Gost_28147_89_KeyExchangeInfo ExportKeyExchange(SafeKeyHandleImpl symKeyHandle, SafeKeyHandleImpl keyExchangeHandle)
        {
            var exportedKeyBytes = ExportCspBlob(symKeyHandle, keyExchangeHandle, Constants.SIMPLEBLOB);

            return DecodeSimpleBlob(exportedKeyBytes);
        }

        private static Gost_28147_89_KeyExchangeInfo DecodeSimpleBlob(byte[] exportedKeyBytes)
        {
            if (exportedKeyBytes == null)
            {
                throw ExceptionUtility.ArgumentNull(nameof(exportedKeyBytes));
            }

            if (exportedKeyBytes.Length < 16)
            {
                throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_DATA);
            }

            if (BitConverter.ToUInt32(exportedKeyBytes, 4) != Constants.CALG_G28147)
            {
                throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_DATA);
            }

            if (BitConverter.ToUInt32(exportedKeyBytes, 8) != Constants.G28147_MAGIC)
            {
                throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_DATA);
            }

            if (BitConverter.ToUInt32(exportedKeyBytes, 12) != Constants.CALG_G28147)
            {
                throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_DATA);
            }

            var keyExchangeInfo = new Gost_28147_89_KeyExchangeInfo();

            var sourceIndex = 16;
            keyExchangeInfo.Ukm = new byte[8];
            Array.Copy(exportedKeyBytes, sourceIndex, keyExchangeInfo.Ukm, 0, 8);
            sourceIndex += 8;

            keyExchangeInfo.EncryptedKey = new byte[32];
            Array.Copy(exportedKeyBytes, sourceIndex, keyExchangeInfo.EncryptedKey, 0, 32);
            sourceIndex += 32;

            keyExchangeInfo.Mac = new byte[4];
            Array.Copy(exportedKeyBytes, sourceIndex, keyExchangeInfo.Mac, 0, 4);
            sourceIndex += 4;

            var encryptionParamSet = new byte[exportedKeyBytes.Length - sourceIndex];
            Array.Copy(exportedKeyBytes, sourceIndex, encryptionParamSet, 0, exportedKeyBytes.Length - sourceIndex);
            keyExchangeInfo.EncryptionParamSet = Gost_28147_89_KeyExchangeInfo.DecodeEncryptionParamSet(encryptionParamSet);

            return keyExchangeInfo;
        }

        #endregion

        #region Для импорта ключей криптографического провайдера

        public static int ImportCspBlob(byte[] importedKeyBytes, SafeProvHandleImpl providerHandle, SafeKeyHandleImpl publicKeyHandle, out SafeKeyHandleImpl keyExchangeHandle)
        {
            var dwFlags = MapCspKeyFlags(CspProviderFlags.NoFlags);
            var keyExchangeRef = SafeKeyHandleImpl.InvalidHandle;

            if (!CryptoApi.CryptImportKey(providerHandle, importedKeyBytes, (uint)importedKeyBytes.Length, publicKeyHandle, dwFlags, ref keyExchangeRef))
            {
                throw CreateWin32Error();
            }

            var keyNumberMask = BitConverter.ToInt32(importedKeyBytes, 4) & 0xE000;
            var keyNumber = (keyNumberMask == 0xA000) ? Constants.AT_KEYEXCHANGE : Constants.AT_SIGNATURE;

            keyExchangeHandle = keyExchangeRef;

            return keyNumber;
        }

        [SecurityCritical]
        public static byte[] EncodePublicBlob(Gost_R3410_KeyExchangeParams publicKeyParameters, int keySize, int signatureAlgId)
        {
            var encodedKeyParams = publicKeyParameters.EncodeParameters();
            var encodedKeyBlob = new byte[16 + encodedKeyParams.Length + publicKeyParameters.PublicKey.Length];
            encodedKeyBlob[0] = 6;
            encodedKeyBlob[1] = 32;
            Array.Copy(BitConverter.GetBytes(signatureAlgId), 0, encodedKeyBlob, 4, 4);
            Array.Copy(BitConverter.GetBytes(Constants.GR3410_1_MAGIC), 0, encodedKeyBlob, 8, 4);
            Array.Copy(BitConverter.GetBytes(keySize), 0, encodedKeyBlob, 12, 4);
            Array.Copy(encodedKeyParams, 0, encodedKeyBlob, 16, encodedKeyParams.Length);
            Array.Copy(publicKeyParameters.PublicKey, 0, encodedKeyBlob, 16 + encodedKeyParams.Length, publicKeyParameters.PublicKey.Length);

            return encodedKeyBlob;
        }

        public static SafeKeyHandleImpl ImportKeyExchange(SafeProvHandleImpl providerHandle, Gost_28147_89_KeyExchangeInfo keyExchangeInfo, SafeKeyHandleImpl keyExchangeHandle)
        {
            if (keyExchangeInfo == null)
            {
                throw ExceptionUtility.ArgumentNull(nameof(keyExchangeInfo));
            }

            var importedKeyBytes = EncodeSimpleBlob(keyExchangeInfo);

            SafeKeyHandleImpl hKeyExchange;
            ImportCspBlob(importedKeyBytes, providerHandle, keyExchangeHandle, out hKeyExchange);

            return hKeyExchange;
        }

        public static SafeKeyHandleImpl ImportBulkSessionKey(ProviderType providerType, SafeProvHandleImpl providerHandle, byte[] bulkSessionKey, RNGCryptoServiceProvider randomNumberGenerator)
        {
            if (bulkSessionKey == null)
            {
                throw ExceptionUtility.ArgumentNull(nameof(bulkSessionKey));
            }

            if (randomNumberGenerator == null)
            {
                throw ExceptionUtility.ArgumentNull(nameof(randomNumberGenerator));
            }

            var hSessionKey = SafeKeyHandleImpl.InvalidHandle;

            if (!CryptoApi.CryptGenKey(providerHandle, Constants.CALG_G28147, 0, ref hSessionKey))
            {
                throw CreateWin32Error();
            }

            var keyWrap = new Gost_28147_89_KeyExchangeInfo { EncryptedKey = new byte[32] };
            Array.Copy(bulkSessionKey, keyWrap.EncryptedKey, 32);
            SetKeyParameterInt32(hSessionKey, Constants.KP_MODE, Constants.CRYPT_MODE_ECB);
            SetKeyParameterInt32(hSessionKey, Constants.KP_ALGID, Constants.CALG_G28147);
            SetKeyParameterInt32(hSessionKey, Constants.KP_PADDING, Constants.ZERO_PADDING);

            uint sessionKeySize = 32;

            if (!CryptoApi.CryptEncrypt(hSessionKey, SafeHashHandleImpl.InvalidHandle, true, 0, keyWrap.EncryptedKey, ref sessionKeySize, sessionKeySize))
            {
                throw CreateWin32Error();
            }

            SetKeyParameterInt32(hSessionKey, Constants.KP_MODE, Constants.CRYPT_MODE_CFB);

            var hashHandle = CreateHashImit(providerHandle, hSessionKey);

            keyWrap.Ukm = new byte[8];
            randomNumberGenerator.GetBytes(keyWrap.Ukm);

            if (!CryptoApi.CryptSetHashParam(hashHandle, Constants.HP_HASHSTARTVECT, keyWrap.Ukm, 0))
            {
                throw CreateWin32Error();
            }

            if (!CryptoApi.CryptHashData(hashHandle, bulkSessionKey, 32, 0))
            {
                throw CreateWin32Error();
            }

            keyWrap.Mac = EndHashData(hashHandle);
            keyWrap.EncryptionParamSet = GetKeyParameterString(hSessionKey, Constants.KP_CIPHEROID);

            SetKeyExchangeExportAlgId(providerType, hSessionKey, Constants.CALG_SIMPLE_EXPORT);
            SetKeyParameterInt32(hSessionKey, Constants.KP_MODE, Constants.CRYPT_MODE_ECB);
            SetKeyParameterInt32(hSessionKey, Constants.KP_PADDING, Constants.ZERO_PADDING);

            return ImportKeyExchange(providerHandle, keyWrap, hSessionKey);
        }

        private static byte[] EncodeSimpleBlob(Gost_28147_89_KeyExchangeInfo keyExchangeInfo)
        {
            if (keyExchangeInfo == null)
            {
                throw ExceptionUtility.ArgumentNull(nameof(keyExchangeInfo));
            }

            var encryptionParamSet = Gost_28147_89_KeyExchangeInfo.EncodeEncryptionParamSet(keyExchangeInfo.EncryptionParamSet);
            var importedKeyBytes = new byte[encryptionParamSet.Length + 60];

            var sourceIndex = 0;
            importedKeyBytes[sourceIndex] = 1;
            sourceIndex++;

            importedKeyBytes[sourceIndex] = 32;
            sourceIndex++;
            sourceIndex += 2;

            Array.Copy(BitConverter.GetBytes(Constants.CALG_G28147), 0, importedKeyBytes, sourceIndex, 4);
            sourceIndex += 4;

            Array.Copy(BitConverter.GetBytes(Constants.G28147_MAGIC), 0, importedKeyBytes, sourceIndex, 4);
            sourceIndex += 4;

            Array.Copy(BitConverter.GetBytes(Constants.CALG_G28147), 0, importedKeyBytes, sourceIndex, 4);
            sourceIndex += 4;

            Array.Copy(keyExchangeInfo.Ukm, 0, importedKeyBytes, sourceIndex, 8);
            sourceIndex += 8;

            Array.Copy(keyExchangeInfo.EncryptedKey, 0, importedKeyBytes, sourceIndex, 32);
            sourceIndex += 32;

            Array.Copy(keyExchangeInfo.Mac, 0, importedKeyBytes, sourceIndex, 4);
            sourceIndex += 4;

            Array.Copy(encryptionParamSet, 0, importedKeyBytes, sourceIndex, encryptionParamSet.Length);

            return importedKeyBytes;
        }

        #endregion

        #region Для работы с цифровой подписью

        public static byte[] SignValue(SafeProvHandleImpl providerHandle, SafeHashHandleImpl hashHandle, int keyNumber, byte[] hashValue)
        {
            SetHashValue(hashHandle, hashValue);

            uint signatureLength = 0;

            // Вычисление размера подписи
            if (!CryptoApi.CryptSignHash(hashHandle, (uint)keyNumber, null, 0, null, ref signatureLength))
            {
                throw CreateWin32Error();
            }

            var signatureValue = new byte[signatureLength];

            // Вычисление значения подписи
            if (!CryptoApi.CryptSignHash(hashHandle, (uint)keyNumber, null, 0, signatureValue, ref signatureLength))
            {
                throw CreateWin32Error();
            }

            return signatureValue;
        }

        public static bool VerifySign(SafeProvHandleImpl providerHandle, SafeHashHandleImpl hashHandle, SafeKeyHandleImpl keyHandle, byte[] hashValue, byte[] signatureValue)
        {
            SetHashValue(hashHandle, hashValue);

            return CryptoApi.CryptVerifySignature(hashHandle, signatureValue, (uint)signatureValue.Length, keyHandle, null, 0);
        }

        private static void SetHashValue(SafeHashHandleImpl hashHandle, byte[] hashValue)
        {
            uint hashLength = 0;

            if (!CryptoApi.CryptGetHashParam(hashHandle, Constants.HP_HASHVAL, null, ref hashLength, 0))
            {
                throw CreateWin32Error();
            }

            if (hashValue.Length != hashLength)
            {
                throw ExceptionUtility.CryptographicException(Constants.NTE_BAD_HASH);
            }

            if (!CryptoApi.CryptSetHashParam(hashHandle, Constants.HP_HASHVAL, hashValue, 0))
            {
                throw CreateWin32Error();
            }
        }

        #endregion

        public static T DangerousAddRef<T>(this T handle) where T : SafeHandle
        {
            var success = false;
            handle.DangerousAddRef(ref success);

            return handle;
        }

        public static void TryDispose(this SafeHandle handle)
        {
            if ((handle != null) && !handle.IsClosed)
            {
                handle.Dispose();
            }
        }

        private static CryptographicException CreateWin32Error()
        {
            return ExceptionUtility.CryptographicException(Marshal.GetLastWin32Error());
        }
    }
}
