Zabezpečení sekcí v konfiguračních souborech

Jan Holan       22.12.2014       Bezpečnost, .NET       12407 zobrazení

Uvažujme tento scénář. Naše klientská .NET aplikace (například WPF nebo Windows Forms) má ve svém app konfiguračním souboru uvedený Connection string, jehož součástí je i uživatelské heslo k připojení k databázi (nelze zde použít trustované připojení). Tato aplikace je umístěna na serveru ve sdílené složce, odkud jí uživatelé spouští. V důsledku toho mají i běžní uživatele přístup do konfiguračního souboru aplikace a vidí v něm uvedené heslo připojení a další informace.

Jak konfigurační soubor zabezpečit, aby nebyl pro uživatelé jednoduše čitelný?

V ASP.NET webových aplikací to můžeme provést vcelku jednoduše. Součástí .NET Frameworku je nástroj aspnet_regiis.exe, kterým je možné zašifrovat uvedenou konfigurační sekci ve web.config souboru. Příkaz pro to může být například tento:

c:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe -pe "connectionStrings" -app "/MojeWebApplikace" -prov "DataProtectionConfigurationProvider"

kde, connectionString je určení konfigurační sekce, kterou chceme zašifrovat (lze uvést i cestu na vnořenou sekci), parametr -app určuje virtuální adresář naší webové aplikace a -prov určuje, který ProtectedConfigurationProvider se pro kryptování použije.

Příkaz upraví connectionString sekci například do toho formátu:

  <connectionStrings configProtectionProvider="DataProtectionConfigurationProvider">
    <EncryptedData>
      <CipherData>
        <CipherValue>AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAA27/xb8gBukm8FetTpWqrEAQAAAACAAAAAAADZgAAwAAAABAAAACcePbsfUYcM/cEFab3IbX3AAAAAASAAACgAAAAEAAAANnuv+NbuJrVd6puIKswuAr4AQAAnpE9VR1ZlTz1GCfgZkG6wJ7yb80xYE6Bxf0a7GjQzZz+pb35x/74F2NMTrXGNLA6RkcUAlhgmi7V/88CUkFiYIHGQbDvt2AaJuI2+fTzFWKaRrCO8V10AKoWpTMAQOR28APrd6paO5udYSJ+njYHpgQWHvQWl1PAJeKWsD8pucNvz5458QKWs8JoMhJUlK6h+X+OYyV93PU4Cq6EFsXVz52AECHrhYjzHpOZcSU1UHBcYhh94ywYq/vUaT82uaKjbSRg8rP3H0hmDlqyXxRpvRqdMCyAB4KlRVn57lEBGjD4Bi3cmVpDJOfDtytn5wfWl58ouRfn8xLYQxPmygg8NSPg7aSo60RbfTNim/aD1rZvTn3b+SspeHd3xoCE3WYJWV6zpb2OIMZnI0pqbxF1xSyBSWRCtUz9dSS2Jx7ZT4nqswz41lRfK4S3uKVCNQe27C1GzYF0NETr2vz0YQVGhbfJX8N3tisYoWfNWYttm4zvMULv/hYvsSfItTnvP6PbArPV+a4DMoODop8xCR6nr6ozHsF/6xymxWi29b3fbd/9JV1OPidgLPyL9Pv6LmWN2q+ZCwNXDkDnYOFV8nRCVZY7/JmCzgNExfUhtfT4Wk10VeVn+DqFN+zNZhFp1rqZJjsNoEZjo6psi4mVsvrorgexP8m4L3NdFAAAAOj9GLixEr/2ojfJkDsGIBN/COeH</CipherValue>
      </CipherData>
    </EncryptedData>
  </connectionStrings>

Obsah sekce connectionString je zašifrován a k sekci je přidán atribut configProtectionProvider určující jakým providerem byla sekce zašifrovaná, aby se vědělo jak sekci dešifrovat. Samozřejmě se nemusí jednat jen o connectionString sekci, tímto způsobem můžeme chránit libovolnou sekci konfiguračního souboru.

V .NET Frameworku jsou implementovány dva tyto protection providery:
DataProtectionConfigurationProvider – Implementován ve třídě DpapiProtectedConfigurationProvider – Používá pro šifrování Windows data protection API (DPAPI).
RsaProtectedConfigurationProvider – Třída RsaProtectedConfigurationProvider – Používá .NET RSA algoritmus (RSACryptoServiceProvider).

U obou providerů se při šifrování ve výchozím nastavení použije klíč odvozený z Machine key. Více informací o šifrování konfigurace webové aplikaci můžete nalézt na MSDN nebo zde nebo například zde.

Náš scénář je ale trochu jiný, jsou zde dva problémy. Jednak se nejedná o webovou aplikaci s web.config souborem, takže přímo aspnet_regiis.exe použít nepůjde. A jednak nemůžeme zašifrování provést pomoci klíče, jehož odvození je závislé na počítači, protože klientskou aplikaci spouští uživatelé z různých počítačů.

Napíšeme si proto vlastní ProtectedConfigurationProvider. Já jsem zvolil jednoduchou metodu, že klíč je natvrdo umístěn přímo v aplikaci, ale dalo by se použít i jiné řešení. Například by se klíč mohl načítat z určeného certifikátu. Kód třídy bude následující:

public class ApplicationProtectedConfigurationProvider : ProtectedConfigurationProvider
{
    #region constants
    private const string cKey = "<base64string key>";
    #endregion

    #region action methods
    public override XmlNode Decrypt(XmlNode encryptedNode)
    {
        string decryptedData = DecryptString(Convert.FromBase64String(cKey), encryptedNode.InnerText);

        var xmlDoc = new XmlDocument();
        xmlDoc.PreserveWhitespace = true;
        xmlDoc.LoadXml(decryptedData);

        return xmlDoc.DocumentElement;
    }

    public override XmlNode Encrypt(XmlNode node)
    {
        string encryptedData = EncryptString(Convert.FromBase64String(cKey), node.OuterXml);

        var xmlDoc = new XmlDocument();
        xmlDoc.PreserveWhitespace = true;
        xmlDoc.LoadXml("<EncryptedData>" + encryptedData + "</EncryptedData>");

        return xmlDoc.DocumentElement;
    }
    #endregion
}

Kód musíme doplnit o klíč v Base64 kódování. Klíč můžeme vygenerovat náhodný například tímto kódem:

string key = Convert.ToBase64String(System.Security.Cryptography.Rijndael.Create().Key);

Ještě nám zbývá doplnit funkce na zašifrování a dešifrování dat, já použiji symetrický Rijndael algoritmus (AES):

#region private member functions
private static string DecryptString(byte[] key, string cipherText)
{
    return System.Text.Encoding.UTF8.GetString(DecryptData(key, Convert.FromBase64String(cipherText)));
}

private static byte[] DecryptData(byte[] key, byte[] data)
{
    int bytesRead;
    byte[] buffer;

    //Open the input stream
    using (MemoryStream inputStream = new MemoryStream(data))
    {
        //Create the CryptoStream for decrypting the data
        using (CryptoStream cryptoStream = OpenDecryptStream(inputStream, key))
        {
            buffer = new byte[data.Length];

            //Read the data from the CryptoStream
            bytesRead = cryptoStream.Read(buffer, 0, buffer.Length);
        }
    }

    //Easy way to get the correctly-sized output array
    using (MemoryStream outputStream = new MemoryStream(buffer, 0, bytesRead))
    {
        return outputStream.ToArray();
    }
}

private static CryptoStream OpenDecryptStream(Stream inputStream, byte[] key)
{
    //Create the algorithm
    Rijndael cryptoAlg = Rijndael.Create();

    //Read the IV from the input stream
    byte[] IV = new byte[cryptoAlg.IV.Length];
    inputStream.Read(IV, 0, IV.Length);

    //Set the key and IV on the algorithm
    cryptoAlg.Key = key;
    cryptoAlg.IV = IV;

    //Create the CryptoStream for decrypting the data
    CryptoStream cryptoStream = new CryptoStream(inputStream, cryptoAlg.CreateDecryptor(), CryptoStreamMode.Read);

    return cryptoStream;
}

private static string EncryptString(byte[] key, string openText)
{
    return Convert.ToBase64String(EncryptData(key, System.Text.Encoding.UTF8.GetBytes(openText)));
}

private static byte[] EncryptData(byte[] key, byte[] data)
{
    //Create the output stream
    using (MemoryStream dataStream = new MemoryStream())
    {
        //Create the CryptoStream
        using (CryptoStream cryptoStream = OpenEncryptStream(dataStream, key))
        {
            //Write the data to the CryptoStream
            cryptoStream.Write(data, 0, data.Length);

            //Flush the final block of data and close the CryptoStream
            cryptoStream.FlushFinalBlock();
        }

        return dataStream.ToArray();
    }
}

private static CryptoStream OpenEncryptStream(Stream outputStream, byte[] key)
{
    //Create and configure the algorithm
    Rijndael cryptoAlg = Rijndael.Create();
    cryptoAlg.Key = key;

    //Write the IV to the output stream unencrypted
    outputStream.Write(cryptoAlg.IV, 0, cryptoAlg.IV.Length);

    //Create the CryptoStream
    CryptoStream cryptoStream = new CryptoStream(outputStream, cryptoAlg.CreateEncryptor(), CryptoStreamMode.Write);

    return cryptoStream;
}
#endregion

Kompletní třídu providera ApplicationProtectedConfigurationProvider umístíme přímo do assembly naší aplikace.

Nyní ještě implementujeme kód, který zajistí automatické zašifrovaní sekce connectionString pomoci našeho protection providera. Kód pak budeme volat při startu aplikace:

internal static class ApplicationConfigurationProtection
{
    #region constants
    private const string cProtectedConfigurationProviderName = "ApplicationProtectedConfigurationProvider";
    #endregion

    #region action methods
    public static bool ProtectConnectionStringsConfigurationSection()
    {
        return ProtectConfigurationSection("connectionStrings");
    }

    public static bool ProtectConfigurationSection(string sectionName)
    {
        var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
        var configSection = config.GetSection(sectionName);

        if (configSection == null)
        {
            throw new ArgumentException(string.Format("Cannot load the configuration section '{0}'.", sectionName), "sectionName");
        }

        if (!configSection.SectionInformation.IsProtected && !configSection.ElementInformation.IsLocked)
        {
            Trace.WriteLine(string.Format("ApplicationConfigurationProtection - About to encrypt unprotected section '{0}'.", sectionName));

            var protectedConfigurationSection = (ProtectedConfigurationSection)config.GetSection("configProtectedData");
            if (protectedConfigurationSection != null && protectedConfigurationSection.Providers[cProtectedConfigurationProviderName] == null)
            {
                //Add ApplicationProtectedConfigurationProvider
                string protectedConfigurationProviderType = GetProtectedConfigurationProviderType(cProtectedConfigurationProviderName);
                if (protectedConfigurationProviderType == null)
                {
                    throw new InvalidOperationException(string.Format("Cannot find type '{0}' in entry assembly.", cProtectedConfigurationProviderName));
                }

                var settings = new ProviderSettings(cProtectedConfigurationProviderName, protectedConfigurationProviderType);
                settings.Parameters["name"] = cProtectedConfigurationProviderName;

                protectedConfigurationSection.Providers.Add(settings);
                protectedConfigurationSection.DefaultProvider = cProtectedConfigurationProviderName;
            }

            //Protect the section
            configSection.SectionInformation.ProtectSection(cProtectedConfigurationProviderName);
            configSection.SectionInformation.ForceSave = true;
            config.Save(ConfigurationSaveMode.Modified);

            Trace.WriteLine(string.Format("ApplicationConfigurationProtection - Successfully encrypted section '{0}'.", sectionName));
            return true;
        }

        return false;
    }
    #endregion

    #region private member functions
    private static string GetProtectedConfigurationProviderType(string protectedConfigurationProviderName)
    {
        Type providerType = Assembly.GetEntryAssembly().GetTypes().FirstOrDefault(t => t.Name == cProtectedConfigurationProviderName);
        if (providerType != null)
        {
            return providerType.FullName + ", " + providerType.Assembly.GetName().Name;
        }

        return null;
    }
    #endregion
}
//Encrypt ConnectionStrings config section
ApplicationConfigurationProtection.ProtectConnectionStringsConfigurationSection();

Metoda ProtectConfigurationSection nejprve pomoci vlastnosti SectionInformation.IsProtected provede kontrolu, zda je v konfiguračním souboru požadovaná sekce již zašifrovaná, pokud ne, tak jí zašifruje a změny uloží do konfiguračního souboru. Abychom nemuseli našeho ApplicationConfigurationProtection ručně registrovat, je zde ještě jedna část kódu, která do konfiguračního souboru rovnou přidá i jeho registraci do sekce configProtectedData (typ providera vyhledá pomoci reflection v aktuální assembly).

Například takto bude tedy vypadat část konfiguračního souboru, kterou kód při prvním spuštění doplní:

  <configProtectedData defaultProvider="ApplicationProtectedConfigurationProvider">
    <providers>
      <add name="ApplicationProtectedConfigurationProvider" type="H2net.Configuration.ApplicationProtectedConfigurationProvider, SQLEditace" />
    </providers>
  </configProtectedData>
  <connectionStrings configProtectionProvider="ApplicationProtectedConfigurationProvider">
    <EncryptedData>KqCyNcfAbsmxBO2Rc6MXonFr5qkK9MGykbZZFZj63ysyRvrbyOLyqXhO3im881Mus7SZCkfIzvweuvWGvjWXZkxngZjztSCJi9xuyT1XrfYNnEd1FPjTNC6D7fLVbcE0MSxlSuIcr+7rUaQseuZ8NI2CKX1oJFEo/ZL8z0vw0y7LHL9SszaY+ANqbjBED8jOCukT3JnoVvm8LGJRXd0E3s+XHJNoUpSxEEQnJFqqoiGT73e8erX7uKPhXSLpK1Ko4i7RqTnBl0dlVPkw2BOgpRqSnPy6BHeVKeZZBHKxISUAR6ogNAE+T/3FhpgEXt33</EncryptedData>
  </connectionStrings>

 

hodnocení článku

-1 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

Klíč

Klíč natvrdo umístěný v kódu? Bravo! Článek má díky tomu téměř nulovou informační hodnotu. Mnohem zajímavější by bylo rozvést problematiku šifrování/dešifrování pomocí klíče umístěného v systému a jak zajistit instalaci tohoto klíče před použitím aplikace.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Nemyslím si, že má článek "téměř nulovou informační hodnotu". Hlavním tématem článku je, že existuje něco jako ProtectedConfigurationProvider.

Že se dají přímo využít buď jeho hotové implementace v .NET Frameworku DataProtectionConfigurationProvider a RsaProtectedConfigurationProvider pro ASP.NET aplikace, a že když je jiný scénář, tak že se dá jednoduše napsat tento provider vlastní.

Ještě k tomu klíči v kódu natvrdo, souhlasím s tím, že to není ideální, ale jsou scénáře kde toto jako zabezpečení postačuje, protože běžný uživatel si nebude na aplikaci pouštět .NET Reflektor a hledat v ní klíč (pokud bude vůbec vědět že ho tam má hledat).

Naopak v mém (popisovaném) scénáři bylo potřeba, aby si aplikaci mohl se síťového share spouštět kdokoliv na libovolném počítači a nemusel si nějaký klíč předem na něj instalovat, nebo něco někde nastavovat (některé tyto počítače ani nejdou v doméně, takže varianta nastavení přes politiky také padá).

Tím samozřejmě ale netvrdím, že je to takto jediná varianta, a samozřejmě vám (ani nikomu jinému) nezakazuji si napsat jiný ProtectedConfigurationProvider, který může vašemu scénaři vyhovovat více a může být bezpečnější. Kód pro automatické použití Providera při startu aplikace je obecný i pro jiné vlastní řešení, takže ho můžete použít.

Vždy je to kompromis mezi tím, jak moc zabezpečené ono nebo jiné řešení je oproti tomu kolik je zákazník za něj ochotný zaplatit.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Změny

A jak poté řešíte, když potřebujete například změnit pouze název/IP adresu serveru či některý z atributů connection stringu? Vždy děláte novou verzi/build aplikace?

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Nerozumím otázce.

Pokud do konfigu napiši jiný connection string, tak si ho aplikace při prvním spuštění zase znovu zašifruje a dál to zase nemusím nijak měnit natož něco buidovat.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback