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>