|
One of the biggest concerns of .NET Developers (and their
clients) is protecting sensitive configuration data such as usernames
and passwords, database connection strings and other global data that
needs to be made easily available application - wide. Of course, web.config
doesn't normally get served by IIS, provided the default mappings are
set in machine.config and not overriden in a local web.config file. But
a lot of people I"ve talked to don't feel comfortable having sensitive
data in web.config. So of course, the first step would be that we can
encrypt the data for starters.
But how to make the data you need available from within
a class library running in the same App Domain, not just from within
your web pages? DLL's can't read configuration data, and they certainly
can't read web.config without writing a custom class to physically load
the web.config or another xml configuration document and do some serious
XPATH. That's overkill, since web.config is already loaded automatically
when the application first starts, and the data in it is cached.
There are a number of solutions to this problem, varying
in complexity and speed. My personal philosophy is that if a hacker can
somehow get to read your web.config file, he's just as likely at that
point to be able to execute arbitrary code of his choice so its already
"too late". Unfortunately, lots of people don't fully understand
the Windows/IIS/.NET security model and because of their naivete, they
naturally decide that they want "overkill" when security comes
into the fray. The result can be an extraordinary amount of additional
coding, and a potential slowdown in the application's performance.
In my recent article on Creating
an XML-Safe DES Encryption Class, I detail how to create a class library
that can encrypt any string of data and Base64 encode the result, making
it safe to store in an XML element without fear of having illegal characters
in the stored string. Here I will present a reasonably elegant solution
to the configuration data problem - one that keeps your data encrypted,
makes it available instantly, and - above all - makes data of almost
any type available from memory, both from your web pages and from your
related assemblies.
The key to the technique is based on the expectation
that all your DLL's will be called from some process that originated with
a web user and therefore will be running in the same App Domain that the
web application created when the first request came in. The System.AppDomain
base class is one that is worthy of considerable study.
Application domains, which are represented by AppDomain
objects, provide isolation, unloading, and security boundaries for executing
managed code.
Multiple application domains can run in a single process;
however, there is not a one-to-one correlation between application domains
and threads. Several threads can belong to a single application domain,
and while a given thread is not confined to a single application domain,
at any given time, a thread executes in a single application domain.
Among many other features, AppDomain offers a CurrentDomain
property that exposes several interesting methods, two of which are GetData()
and SetData().
AppDomain exposes a set of predefined items:
| Value of 'name' |
Property |
| "APPBASE" |
ApplicationBase |
| "APP_CONFIG_FILE" |
ConfigurationFile |
| "DYNAMIC_BASE" |
DynamicBase |
| "DEV_PATH" |
(no property) |
| "APP_NAME" |
ApplicationName |
| "CACHE_BASE" |
PrivateBinPath |
| "BINPATH_PROBE_ONLY" |
PrivateBinPathProbe |
| "SHADOW_COPY_DIRS" |
ShadowCopyDirectories |
| "FORCE_CACHE_INSTALL" |
ShadowCopyFiles |
| "CACHE_BASE" |
CachePath |
| (application specific) |
LicenseFile |
However, in our case we are more interested in the fact
that the GetData and SetData methods allow us to store any data we want
- objects, types, anything that supports ISerializable. Not only that,
but by simply issuing the call:
System.AppDomain.CurrentDomain.GetData("connectionstring")
- we can pull our data out from anywhere in the AppDomain
- whether it's the code in an ASPX or ASMX page or a custom class library
we've called in our code.
So the key to the whole scheme would now go as follows
(you most likely have it already figured out, if you've been with me):
1) Encrypt the stuff we need and store it in web.config
in the appSettings section, which allows us to use ConfigurationSettings.AppSettings("ABCD")
to easily retrieve our data.
2) After reading the data out in Global.asax in the Sub Application_Start,
we decrypt it using our secret key, and store it immediately with name,
value pairs in the System.AppDomain cache.
3) Anytime we need the data, we simply call System.AppDomain.CurrentDomain.GetData("ABCD")
and we're done.
Here is a sample of some custom keys I've stored in the
appSettings section of my web.config:
<?xml version="1.0"
encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- Encrypt your data using a key that you remember and place encrypted
data in new keys in this section -->
<add key="ABCD" value="Ni/6UHcq+3r5eEerZ00Kbbd4WDzaH1zx"
/>
<add key="EFGH" value="1CejuTBT3xd6PXj3v2pIvtE0oDTDKcFH"
/>
<add key="IJKL" value="P0FjG5sL5k19aTsI1hHULEVqbU0Tq2SthExjOL
wIWfGxDfoe83B4POdjEJ/deRG3+z0J0lEtNAGLWKIBjzKoFlr1FQ2EekcWq79NaFVLZ5nbMOuHSwwKgYxqXOkA6GNw"
/>
</appSettings>
<system.web>
the Application_Start
code in global.asax would look like so:
Sub
Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Retrieve Global app settings from Web.config, decrypt, and put in
' AppDomain cache where they are available to all classes in the AppDomain...
Dim fe As Encryption64 = New Encryption64()
Dim ABCD As String = fe.DecryptFromBase64String(ConfigurationSettings.AppSettings("ABCD"),
"12345678")
System.AppDomain.CurrentDomain.SetData("ABCD", ABCD)
Dim EFGH As String = fe.DecryptFromBase64String(ConfigurationSettings.AppSettings("EFGH"),
"12345678")
System.AppDomain.CurrentDomain.SetData("EFGH", EFGH)
Dim IJKL As String = fe.DecryptFromBase64String(ConfigurationSettings.AppSettings("IJKL"),
"12345678")
System.AppDomain.CurrentDomain.SetData("IJKL", IJKL)
fe = Nothing
End Sub
The
call you see to "fe" as Encryption64 is a call to my encryption
class. The "12345678" you see as the last method parameter is
the "Secret key"
that is used to both encrypt and decrypt the data. In the event that you
decide you don't want your decryption key exposed in global.asax, you
could simply make a call to a small assembly whose sole purpose is to
return your decryption key. The key value inside the assembly would be
encrypted itself, and part of the function that returns it would be to
decrypt it first. There's a copy of the .NET dll in the code download
for this article; if you want to see the source, visit the article I mentioned
above.
The test web page that is included in the
download has simple code as follows:
Response.Write("Encrypted
global data:<BR>")
Response.Write("ABCD =" & System.AppDomain.CurrentDomain.GetData("ABCD")
& "<BR>")
Response.Write("EFGH =" & System.AppDomain.CurrentDomain.GetData("EFGH")
& "<BR>")
Response.Write("IJKL =" & System.AppDomain.CurrentDomain.GetData("IJKL")
& "<BR>")
And the result you'll see
when you run the page is:
Encrypted global data:
ABCD =PETER\PrivilegedUser
EFGH =PrivilegedPassword
IJKL =Provider=SQLOLEDB.1;Data Source=YourServer;Initial Catalog=YourDatabase;Trusted
Connection=Yes
You now have
in your hands a simple, very fast, and extensible method for protecting
private data and making it available to your components from an App-Domain
- wide in-memory cache.
Download
the code that accompanies this article
Peter Bromberg is an independent consultant specializing in distributed .NET solutions
Inc. in Orlando and a co-developer of the NullSkull.com
developer website. He can be reached at info@eggheadcafe.com
|