// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Specialized;
using System.Security.Cryptography;
using System.Text;
using System.Xml;

namespace System.Configuration
{
    public sealed class DpapiProtectedConfigurationProvider : ProtectedConfigurationProvider
    {
        private const int CRYPTPROTECT_UI_FORBIDDEN = 0x1;
        private const int CRYPTPROTECT_LOCAL_MACHINE = 0x4;
        private bool _useMachineProtection = true;
        private string _keyEntropy;

        public override XmlNode Decrypt(XmlNode encryptedNode)
        {
            if (encryptedNode.NodeType != XmlNodeType.Element ||
                encryptedNode.Name != "EncryptedData")
            {
                throw new ConfigurationErrorsException(SR.DPAPI_bad_data);
            }

            XmlNode cipherNode = TraverseToChild(encryptedNode, "CipherData", false);
            if (cipherNode == null)
                throw new ConfigurationErrorsException(SR.DPAPI_bad_data);

            XmlNode cipherValue = TraverseToChild(cipherNode, "CipherValue", true);
            if (cipherValue == null)
                throw new ConfigurationErrorsException(SR.DPAPI_bad_data);

            string encText = cipherValue.InnerText;
            if (encText == null)
                throw new ConfigurationErrorsException(SR.DPAPI_bad_data);

            string decText = DecryptText(encText);
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.PreserveWhitespace = true;
            xmlDocument.LoadXml(decText);
            return xmlDocument.DocumentElement;
        }

        public override XmlNode Encrypt(XmlNode node)
        {
            string text = node.OuterXml;
            string encText = EncryptText(text);
            string pre = @"<EncryptedData><CipherData><CipherValue>";
            string post = @"</CipherValue></CipherData></EncryptedData>";
            string xmlText = pre + encText + post;

            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.PreserveWhitespace = true;
            xmlDocument.LoadXml(xmlText);
            return xmlDocument.DocumentElement;
        }

        private string EncryptText(string clearText)
        {
            if (clearText == null || clearText.Length < 1)
                return clearText;

            byte[] inputData = PrepareDataBlob(clearText);
            byte[] entropyData = PrepareDataBlob(_keyEntropy);

            byte[] encryptedData = ProtectedData.Protect(
                userData: inputData,
                optionalEntropy: entropyData,
                scope: UseMachineProtection ? DataProtectionScope.LocalMachine : DataProtectionScope.CurrentUser);

            return Convert.ToBase64String(encryptedData);
        }

        private string DecryptText(string encText)
        {
            if (encText == null || encText.Length < 1)
                return encText;

            byte[] inputData = Convert.FromBase64String(encText);
            byte[] entropyData = PrepareDataBlob(_keyEntropy);

            byte[] decryptedData = ProtectedData.Unprotect(
                encryptedData: inputData,
                optionalEntropy: entropyData,
                scope: UseMachineProtection ? DataProtectionScope.LocalMachine : DataProtectionScope.CurrentUser);

            return Encoding.Unicode.GetString(decryptedData);
        }

        public bool UseMachineProtection { get { return _useMachineProtection; } }

        public override void Initialize(string name, NameValueCollection configurationValues)
        {
            base.Initialize(name, configurationValues);
            _useMachineProtection = GetBooleanValue(configurationValues, "useMachineProtection", true);
            _keyEntropy = configurationValues["keyEntropy"];
            configurationValues.Remove("keyEntropy");
            if (configurationValues.Count > 0)
                throw new ConfigurationErrorsException(SR.Format(SR.Unrecognized_initialization_value, configurationValues.GetKey(0)));
        }

        private static XmlNode TraverseToChild(XmlNode node, string name, bool onlyChild)
        {
            foreach (XmlNode child in node.ChildNodes)
            {
                if (child.NodeType != XmlNodeType.Element)
                    continue;
                if (child.Name == name)
                    return child; // found it!
                if (onlyChild)
                    return null;
            }

            return null;
        }

        private static byte[] PrepareDataBlob(string s)
        {
            return (s != null) ? Encoding.Unicode.GetBytes(s) : Array.Empty<byte>();
        }

        private static bool GetBooleanValue(NameValueCollection configurationValues, string valueName, bool defaultValue)
        {
            string s = configurationValues[valueName];
            if (s == null)
                return defaultValue;
            configurationValues.Remove(valueName);
            if (s == "true")
                return true;
            if (s == "false")
                return false;
            throw new ConfigurationErrorsException(SR.Format(SR.Config_invalid_boolean_attribute, valueName));
        }
    }
}
