/*************************************************************************************************
  Required Notice: Copyright (C) EPPlus Software AB. 
  This software is licensed under PolyForm Noncommercial License 1.0.0 
  and may only be used for noncommercial purposes 
  https://polyformproject.org/licenses/noncommercial/1.0.0/

  A commercial license to use this software can be purchased at https://epplussoftware.com
 *************************************************************************************************
  Date               Author                       Change
 *************************************************************************************************
  01/27/2020         EPPlus Software AB       Initial release EPPlus 5
 *************************************************************************************************/
using OfficeOpenXml.Utils;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;

namespace OfficeOpenXml.Encryption
{
    internal abstract class EncryptionInfo
    {
        internal short MajorVersion;
        internal short MinorVersion;
        internal abstract void Read(byte[] data);

        internal static EncryptionInfo ReadBinary(byte[] data)
        {
            var majorVersion = BitConverter.ToInt16(data, 0);
            var minorVersion = BitConverter.ToInt16(data, 2);
            EncryptionInfo ret;
            if ((minorVersion == 2 || minorVersion == 3) && majorVersion <= 4) // minorVersion==1 is RC4, not supported.
            {
                ret = new EncryptionInfoBinary();
            }
            else if (majorVersion == 4 && minorVersion==4)
            {
                ret = new EncryptionInfoAgile();
            }
            else
            {
                throw (new NotSupportedException("Unsupported encryption format"));
            }
            ret.MajorVersion = majorVersion;
            ret.MinorVersion = minorVersion;
            ret.Read(data);
            return ret;
        }
    }
    internal enum eCipherAlgorithm
    {
        /// <summary>
        /// AES. MUST conform to the AES algorithm.
        /// </summary>
        AES,
        /// <summary>
        /// RC2. MUST conform to [RFC2268].
        /// </summary>
        RC2,
        /// <summary>
        /// RC4. 
        /// </summary>
        RC4,
        /// <summary>
        /// MUST conform to the DES algorithm.
        /// </summary>
        DES,
        /// <summary>
        /// MUST conform to the [DRAFT-DESX] algorithm.
        /// </summary>
        DESX,
        /// <summary>
        /// 3DES. MUST conform to the [RFC1851] algorithm. 
        /// </summary>
        TRIPLE_DES,
        /// 3DES_112 MUST conform to the [RFC1851] algorithm. 
        TRIPLE_DES_112        
    }
    internal enum eChainingMode
    {
        /// <summary>
        /// Cipher block chaining (CBC).
        /// </summary>
        ChainingModeCBC,
        /// <summary>
        /// Cipher feedback chaining (CFB), with 8-bit window.
        /// </summary>
        ChainingModeCFB
    }
    /// <summary>
    /// Hash algorithm
    /// </summary>
    internal enum eHashAlgorithm
    {
        /// <summary>
        /// Sha 1-MUST conform to [RFC4634]
        /// </summary>
        SHA1,
        /// <summary>
        /// Sha 256-MUST conform to [RFC4634]
        /// </summary>
        SHA256,
        /// <summary>
        /// Sha 384-MUST conform to [RFC4634]
        /// </summary>
        SHA384,
        /// <summary>
        /// Sha 512-MUST conform to [RFC4634]
        /// </summary>
        SHA512,
        /// <summary>
        /// MD5
        /// </summary>
        MD5,
        /// <summary>
        /// MD4
        /// </summary>
        MD4,
        /// <summary>
        /// MD2
        /// </summary>
        MD2,
        /// <summary>
        /// RIPEMD-128 MUST conform to [ISO/IEC 10118]
        /// </summary>
        RIPEMD128,
        /// <summary>
        /// RIPEMD-160 MUST conform to [ISO/IEC 10118]
        /// </summary>
        RIPEMD160,
        /// <summary>
        /// WHIRLPOOL MUST conform to [ISO/IEC 10118]
        /// </summary>
        WHIRLPOOL
    }
    /// <summary>
    /// Handels the agile encryption
    /// </summary>
    internal class EncryptionInfoAgile : EncryptionInfo
    {
        XmlNamespaceManager _nsm;
        public EncryptionInfoAgile()
        {
            var nt = new NameTable();
            _nsm = new XmlNamespaceManager(nt);
            _nsm.AddNamespace("d", "http://schemas.microsoft.com/office/2006/encryption");
            _nsm.AddNamespace("c", "http://schemas.microsoft.com/office/2006/keyEncryptor/certificate");
            _nsm.AddNamespace("p", "http://schemas.microsoft.com/office/2006/keyEncryptor/password");
        }
        internal class EncryptionKeyData : XmlHelper
        {
            public EncryptionKeyData(XmlNamespaceManager nsm, XmlNode topNode) :
                base(nsm, topNode)
            {

            }
            internal byte[] SaltValue
            {
                get
                {
                    var s = GetXmlNodeString("@saltValue");
                    if (!string.IsNullOrEmpty(s))
                    {
                        return Convert.FromBase64String(s);
                    }
                    return null;
                }
                set
                {
                    SetXmlNodeString("@saltValue", Convert.ToBase64String(value));
                }
            }
            internal eHashAlgorithm HashAlgorithm
            {
                get
                {
                    return GetHashAlgorithm(GetXmlNodeString("@hashAlgorithm"));
                }
                set
                {
                    SetXmlNodeString("@hashAlgorithm", GetHashAlgorithmString(value));
                }
            }

            private eHashAlgorithm GetHashAlgorithm(string v)
            {
                switch (v)
                {
                    case "RIPEMD-128":
                        return eHashAlgorithm.RIPEMD128;
                    case "RIPEMD-160":
                        return eHashAlgorithm.RIPEMD160;
                    case "SHA-1":
                        return eHashAlgorithm.SHA1;
                    default:
                        try
                        {
                            return (eHashAlgorithm)Enum.Parse(typeof(eHashAlgorithm),v);
                        }
                        catch
                        {
                            throw (new InvalidDataException("Invalid Hash algorithm"));
                        }
                }
            }

            private string GetHashAlgorithmString(eHashAlgorithm value)
            {
                switch (value)
                {
                    case eHashAlgorithm.RIPEMD128:
                        return "RIPEMD-128";
                    case eHashAlgorithm.RIPEMD160:
                        return "RIPEMD-160";
                    case eHashAlgorithm.SHA1:
                        return "SHA-1";
                    default: 
                        return value.ToString();
                }
            }
            internal eChainingMode CipherChaining
            {
                get
                {
                    var v=GetXmlNodeString("@cipherChaining");
                    try
                    {
                        return (eChainingMode)Enum.Parse(typeof(eChainingMode), v);
                    }
                    catch
                    {
                        throw (new InvalidDataException("Invalid chaining mode"));
                    }
                }
                set
                {
                    SetXmlNodeString("@cipherChaining", value.ToString());
                }
            }
            internal eCipherAlgorithm CipherAlgorithm
            {
                get
                {
                    return GetCipherAlgorithm(GetXmlNodeString("@cipherAlgorithm"));
                }
                set
                {
                    SetXmlNodeString("@cipherAlgorithm", GetCipherAlgorithmString(value));
                }
            }

            private eCipherAlgorithm GetCipherAlgorithm(string v)
            {
                switch (v)
                {
                    case "3DES":
                        return eCipherAlgorithm.TRIPLE_DES;
                    case "3DES_112":
                        return eCipherAlgorithm.TRIPLE_DES_112;
                    default:
                        try
                        {
                            return (eCipherAlgorithm)Enum.Parse(typeof(eCipherAlgorithm), v);
                        }
                        catch
                        {
                            throw (new InvalidDataException("Invalid Hash algorithm"));
                        }
                }
            }

            private string GetCipherAlgorithmString(eCipherAlgorithm alg)
            {
                switch (alg)
                {
                    case eCipherAlgorithm.TRIPLE_DES:
                        return "3DES";
                    case eCipherAlgorithm.TRIPLE_DES_112:
                        return "3DES_112";                    
                    default:
                        return alg.ToString();
                }
            }
            internal int HashSize
            {
                get
                {
                    return GetXmlNodeInt("@hashSize");
                }
                set
                {
                    SetXmlNodeString("@hashSize", value.ToString());
                }
            }
            internal int KeyBits
            {
                get
                {
                    return GetXmlNodeInt("@keyBits");
                }
                set
                {
                    SetXmlNodeString("@keyBits", value.ToString());
                }
            }
            internal int BlockSize
            {
                get
                {
                    return GetXmlNodeInt("@blockSize");
                }
                set
                {
                    SetXmlNodeString("@blockSize", value.ToString());
                }
            }
            internal int SaltSize
            {
                get
                {
                    return GetXmlNodeInt("@saltSize");
                }
                set
                {
                    SetXmlNodeString("@saltSize", value.ToString());
                }
            }
        }
        internal class EncryptionDataIntegrity : XmlHelper
        {
            public EncryptionDataIntegrity(XmlNamespaceManager nsm, XmlNode topNode) :
                base(nsm, topNode)
            {

            }
            internal byte[] EncryptedHmacValue
            {
                get
                {
                    var s = GetXmlNodeString("@encryptedHmacValue");
                    if (!string.IsNullOrEmpty(s))
                    {
                        return Convert.FromBase64String(s);
                    }
                    return null;
                }
                set
                {
                    SetXmlNodeString("@encryptedHmacValue", Convert.ToBase64String(value));
                }
            }
            internal byte[] EncryptedHmacKey
            {
                get
                {
                    var s = GetXmlNodeString("@encryptedHmacKey");
                    if (!string.IsNullOrEmpty(s))
                    {
                        return Convert.FromBase64String(s);
                    }
                    return null;
                }
                set
                {
                    SetXmlNodeString("@encryptedHmacKey", Convert.ToBase64String(value));
                }
            }
        }
        internal class EncryptionKeyEncryptor : EncryptionKeyData
        {
            public EncryptionKeyEncryptor(XmlNamespaceManager nsm, XmlNode topNode) :
                base(nsm, topNode)
            {

            }
            internal byte[] EncryptedKeyValue
            {
                get
                {
                    var s = GetXmlNodeString("@encryptedKeyValue");
                    if (!string.IsNullOrEmpty(s))
                    {
                        return Convert.FromBase64String(s);
                    }
                    return null;
                }
                set
                {
                    SetXmlNodeString("@encryptedKeyValue", Convert.ToBase64String(value));
                }
            }
            internal byte[] EncryptedVerifierHash
            {
                get
                {
                    var s = GetXmlNodeString("@encryptedVerifierHashValue");
                    if (!string.IsNullOrEmpty(s))
                    {
                        return Convert.FromBase64String(s);
                    }
                    return null;

                }
                set
                {
                    SetXmlNodeString("@encryptedVerifierHashValue", Convert.ToBase64String(value));
                }
            }
            internal byte[] EncryptedVerifierHashInput
            {
                get
                {
                    var s = GetXmlNodeString("@encryptedVerifierHashInput");
                    if (!string.IsNullOrEmpty(s))
                    {
                        return Convert.FromBase64String(s);
                    }
                    return null;
                }
                set
                {
                    SetXmlNodeString("@encryptedVerifierHashInput", Convert.ToBase64String(value));
                }
            }
            internal byte[] VerifierHashInput { get; set; }
            internal byte[] VerifierHash { get; set; }
            internal byte[] KeyValue { get; set; }
            internal int SpinCount
            {
                get
                {
                    return GetXmlNodeInt("@spinCount");
                }
                set
                {
                    SetXmlNodeString("@spinCount", value.ToString());
                }
            }
        }
        /*
        <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
           <encryption xmlns="http://schemas.microsoft.com/office/2006/encryption" xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password" xmlns:c="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate">
               <keyData saltSize="16" blockSize="16" keyBits="256" hashSize="64" cipherAlgorithm="AES" cipherChaining="ChainingModeCBC" hashAlgorithm="SHA512" saltValue="pa+hrJ3s1zrY6hmVuSa5JQ==" />
               <dataIntegrity encryptedHmacKey="nd8i4sEKjsMjVN2gLo91oFN2e7bhMpWKDCAUBEpz4GW6NcE3hBXDobLksZvQGwLrPj0SUVzQA8VuDMyjMAfVCA==" encryptedHmacValue="O6oegHpQVz2uO7Om4oZijSi4kzLiiMZGIjfZlq/EFFO6PZbKitenBqe2or1REaxaI7gO/JmtJzZ1ViucqTaw4g==" />
               <keyEncryptors>
                   <keyEncryptor uri="http://schemas.microsoft.com/office/2006/keyEncryptor/password">
                      <p:encryptedKey spinCount="100000" saltSize="16" blockSize="16" keyBits="256" hashSize="64" cipherAlgorithm="AES" cipherChaining="ChainingModeCBC" hashAlgorithm="SHA512" saltValue="u2BNFAuHYn3M/WRja3/uPg==" encryptedVerifierHashInput="M0V+fRolJMRgFyI9w+AVxQ==" encryptedVerifierHashValue="V/6l9pFH7AaXFqEbsnFBfHe7gMOqFeRwaNMjc7D3LNdw6KgZzOOQlt5sE8/oG7GPVBDGfoQMTxjQydVPVy4qng==" encryptedKeyValue="B0/rbSQRiIKG5CQDH6AKYSybdXzxgKAfX1f+S5k7mNE=" />
                   </keyEncryptor></keyEncryptors></encryption>
        */
        
        /***
         * <?xml version="1.0" encoding="UTF-8" standalone="true"?>
            <encryption xmlns:c="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate" xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password" xmlns="http://schemas.microsoft.com/office/2006/encryption">
         *      <keyData saltValue="XmTB/XBGJSbwd/GTKzQv5A==" hashAlgorithm="SHA512" cipherChaining="ChainingModeCBC" cipherAlgorithm="AES" hashSize="64" keyBits="256" blockSize="16" saltSize="16"/>
         *      <dataIntegrity encryptedHmacValue="WWw3Bb2dbcNPMnl9f1o7rO0u7sclWGKTXqBA6rRzKsP2KzWS5T0LxY9qFoC6QE67t/t+FNNtMDdMtE3D1xvT8w==" encryptedHmacKey="p/dVdlJY5Kj0k3jI1HRjqtk4s0Y4HmDAsc8nqZgfxNS7DopAsS3LU/2p3CYoIRObHsnHTAtbueH08DFCYGZURg=="/>
         *          <keyEncryptors>
         *              <keyEncryptor uri="http://schemas.microsoft.com/office/2006/keyEncryptor/password">
         *                  <p:encryptedKey saltValue="EeBtY0QftyOkLztCl7NF0g==" hashAlgorithm="SHA512" cipherChaining="ChainingModeCBC" cipherAlgorithm="AES" hashSize="64" keyBits="256" blockSize="16" saltSize="16" encryptedKeyValue="Z7AO8vHnnPZEb1VqyZLJ6JFc3Mq3E322XPxWXS21fbU=" encryptedVerifierHashValue="G7BxbKnZanldvtsbu51mP9J3f9Wr5vCfCpvWSh5eIJff7Sr3J2DzH1/9aKj9uIpqFQIsLohpRk+oBYDcX7hRgw==" encryptedVerifierHashInput="851eszl5y5rdU1RnTjEWHw==" spinCount="100000"/>
         *              </keyEncryptor>
         *      </keyEncryptors>
         *      </encryption
         * ***/
        internal EncryptionDataIntegrity DataIntegrity { get; set; }
        internal EncryptionKeyData KeyData { get; set; }
        internal List<EncryptionKeyEncryptor> KeyEncryptors
        {
            get;
            private set;
        }

        internal XmlDocument Xml {get;set;}
        internal override void Read(byte[] data)
        {
            var byXml = new byte[data.Length - 8];
            Array.Copy(data, 8, byXml, 0, data.Length - 8);
            var xml = Encoding.UTF8.GetString(byXml);
            ReadFromXml(xml);
        }
        internal void ReadFromXml(string xml)
        {
            Xml = new XmlDocument();
            XmlHelper.LoadXmlSafe(Xml, xml, Encoding.UTF8);
            var node = Xml.SelectSingleNode("/d:encryption/d:keyData", _nsm);
            KeyData = new EncryptionKeyData(_nsm, node);
            node = Xml.SelectSingleNode("/d:encryption/d:dataIntegrity", _nsm);
            DataIntegrity = new EncryptionDataIntegrity(_nsm, node);
            KeyEncryptors = new List<EncryptionKeyEncryptor>();

            var list = Xml.SelectNodes("/d:encryption/d:keyEncryptors/d:keyEncryptor/p:encryptedKey", _nsm);
            if (list != null)
            {
                foreach (XmlNode n in list)
                {
                    KeyEncryptors.Add(new EncryptionKeyEncryptor(_nsm, n));
                }
            }

        }
    }
    /// <summary>
    /// Handles the EncryptionInfo stream
    /// </summary>
    internal class EncryptionInfoBinary : EncryptionInfo
    {


        internal Flags Flags;
        internal uint HeaderSize;
        internal EncryptionHeader Header;
        internal EncryptionVerifier Verifier;
        internal override void Read(byte[] data)
        {
            Flags = (Flags)BitConverter.ToInt32(data, 4);
            HeaderSize = (uint)BitConverter.ToInt32(data, 8);

            /**** EncryptionHeader ****/
            Header = new EncryptionHeader();
            Header.Flags = (Flags)BitConverter.ToInt32(data, 12);
            Header.SizeExtra = BitConverter.ToInt32(data, 16);
            Header.AlgID = (AlgorithmID)BitConverter.ToInt32(data, 20);
            Header.AlgIDHash = (AlgorithmHashID)BitConverter.ToInt32(data, 24);
            Header.KeySize = BitConverter.ToInt32(data, 28);
            Header.ProviderType = (ProviderType)BitConverter.ToInt32(data, 32);
            Header.Reserved1 = BitConverter.ToInt32(data, 36);
            Header.Reserved2 = BitConverter.ToInt32(data, 40);

            byte[] text = new byte[(int)HeaderSize - 34];
            Array.Copy(data, 44, text, 0, (int)HeaderSize - 34);
            Header.CSPName = UTF8Encoding.Unicode.GetString(text);

            int pos = (int)HeaderSize + 12;

            /**** EncryptionVerifier ****/
            Verifier = new EncryptionVerifier();
            Verifier.SaltSize = (uint)BitConverter.ToInt32(data, pos);
            Verifier.Salt = new byte[Verifier.SaltSize];

            Array.Copy(data, pos + 4, Verifier.Salt, 0, (int)Verifier.SaltSize);

            Verifier.EncryptedVerifier = new byte[16];
            Array.Copy(data, pos + 20, Verifier.EncryptedVerifier, 0, 16);

            Verifier.VerifierHashSize = (uint)BitConverter.ToInt32(data, pos + 36);
            Verifier.EncryptedVerifierHash = new byte[Verifier.VerifierHashSize];
            Array.Copy(data, pos + 40, Verifier.EncryptedVerifierHash, 0, (int)Verifier.VerifierHashSize);
        }
        internal byte[] WriteBinary()
        {
            using (var ms = RecyclableMemory.GetStream())
            {
                BinaryWriter bw = new BinaryWriter(ms);

                bw.Write(MajorVersion);
                bw.Write(MinorVersion);
                bw.Write((int)Flags);
                byte[] header = Header.WriteBinary();
                bw.Write((uint)header.Length);
                bw.Write(header);
                bw.Write(Verifier.WriteBinary());

                bw.Flush();
                return ms.ToArray();
            }
        }

    }
}
