This section describes the code behind both of the application authentication and the network authentication samples.
The Windows Mobile Professional sample uses a SQL Server Compact database that is encrypted by using a password. The code to set up the database looks like the following.
string applicationPath =
Path.GetDirectoryName(this.GetType().Assembly.GetName().CodeBase);
string localDatabasePath = applicationPath + Path.DirectorySeparatorChar +
"EncryptedDatabase.sdf";
string databasePassword = "pe4eGaWR46a4e+UPR-c??&wa!uFu#asw";
string localConnectionString = "Data Source=" + localDatabasePath +
";Password=" + databasePassword;
string localDatabaseScriptPath = applicationPath +
Path.DirectorySeparatorChar + "EncryptedDatabase.sql";
// Create only if it doesn't exist
if(File.Exists(localDatabasePath))
return;
// Create local database
using(SqlCeEngine engine = new SqlCeEngine(localConnectionString +
";Encrypt Database=True"))
engine.CreateDatabase();
// Add table and user row
using(SqlCeConnection cn = new SqlCeConnection(localConnectionString))
{
cn.Open();
SqlCeCommand cmd = cn.CreateCommand();
StreamReader sr = new StreamReader(localDatabaseScriptPath);
string s;
string commandText = string.Empty;
while(sr.Peek() > 0)
{
s = sr.ReadLine();
if(s.Trim().ToUpper() == "GO")
{
cmd.CommandText = commandText;
cmd.ExecuteNonQuery();
commandText = string.Empty;
}
else
commandText += s;
}
}
First, the code retrieves the application path and uses it to create the full path of both the database itself and a script file, which is used to set up the database. A database password is hard coded, and even though the password can be any value, it is probably best to create it by using a password generation tool. The password generation tool should mix case and include both numbers and punctuation. But you should be observant because some punctuation characters (like an equal sign [=] and a semicolon [;]) are not allowed in a connection string. Remember that the encryption strength is relative to the key length, and that this password will never be entered by anyone, so there is no need for a short and readable password. In the previous code, a 256-bit password is used which should provide very high security.
After the code establishes a database path and the password, a connection string is set up, which creates the database. The connection string needs to include an extra parameter (Encrypt Database=True) when the database is created. This extra parameter is not necessary when the application opens the database later. The loop reads the script file and submits the content to the database.
The script file looks like the following.
CREATE TABLE Users (
UserID uniqueidentifier PRIMARY KEY DEFAULT NEWID() NOT NULL,
Name NVARCHAR(50) NOT NULL,
UserName NVARCHAR(30) NOT NULL,
PasswordHash NVARCHAR(32) NOT NULL,
DomainName NVARCHAR(30) NULL,
EmailAddress NVARCHAR(50) NOT NULL
)
GO
INSERT INTO Users (Name, UserName, PasswordHash, DomainName, EmailAddress)
VALUES('Some One', 'someone', 'fa6a602c680673f462cab11b2b07218e', NULL,
'someone@microsoft.com')
GO
As this code example shows, this script file looks like any database script file; therefore, you can reuse this code in most situations where a local SQL Server Compact database should be initialized.
The observant reader notices that the password column (PasswordHash) has some odd content—the MD5 hash value of the username and the password combined (in this example, the string "someonetest"). Even if standards are used for encryption, it is small individual inventions like this (concatenating the username and password to calculate the hash value) that makes it much harder for anyone to crack the system. In a highly secure environment, only this hash value is stored in any system. The value is generated when the user changes the password, and because there is no way to get to the original password, the password needs to be reset if the user should forget it.
In either version of the .NET Framework (desktop or Compact), the code to generate the MD5 hash value looks like the following.
private string GetMD5(string stringToHash)
{
MD5 md5 = MD5CryptoServiceProvider.Create();
// Convert a string to an array of bytes
byte[] buffer = Encoding.ASCII.GetBytes(stringToHash);
// Create a hash value from the array
byte[] hash = md5.ComputeHash(buffer);
// Convert a hash to a string
string s = string.Empty;
foreach(byte b in hash)
s += b.ToString("x2");
return s;
}
As mentioned previously, if this sample was used with the .NET Framework 1.0 it could use the MD5 implementation by the Flow Group. The code to authenticate (when the user taps the Login button) looks like the following.
// Get the MD5 hash from the user name and password that is provided
string userMD5 = GetMD5(userNameTextBox.Text + passwordTextBox.Text);
// Get the password hash from the database
string applicationPath =
Path.GetDirectoryName(this.GetType().Assembly.GetName().CodeBase);
string localDatabasePath = applicationPath + Path.DirectorySeparatorChar +
"EncryptedDatabase.sdf";
string databasePassword = "pe4eGaWR46a4e+UPR-c??&wa!uFu#asw";
string localConnectionString = "Data Source=" + localDatabasePath +
";Password=" + databasePassword;
string dbMD5;
try
{
using(SqlCeConnection cn = new SqlCeConnection(localConnectionString))
{
cn.Open();
SqlCeCommand cmd = cn.CreateCommand();
cmd.CommandText = "SELECT PasswordHash FROM Users WHERE UserName='" +
userNameTextBox.Text + "'";
dbMD5 = cmd.ExecuteScalar().ToString();
}
// Check password hash
if(userMD5 == dbMD5)
MessageBox.Show("User authorized!", this.Text);
else
MessageBox.Show("User not authorized!", this.Text);
}
catch(SqlCeException ex)
{
string s = ex.Message;
int i = s.IndexOf('[');
if(i > 1)
s = s.Substring(0, i - 2);
MessageBox.Show(s + "!", this.Text, MessageBoxButtons.OK,
MessageBoxIcon.Hand, MessageBoxDefaultButton.Button1);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, this.Text, MessageBoxButtons.OK,
MessageBoxIcon.Hand, MessageBoxDefaultButton.Button1);
}
The code calculates the password hash for the entered username and password, and then the code retrieves the stored hash value from the local database by using the database password. Then, the code compares the two hash values and presents the result to the user. Even if the exception handling that is implemented in the code presents errors (both from the database and other) in a user-friendly way to the user, it is important not to provide too much help because an unauthorized person may be trying to force the authentication. Therefore, messages like "the username was correct, but not the password" or even "username not found" should be avoided.
On the Smartphone, although SQL Server Compact could be used as well, the data is provided in an XML file (from a DataSet object) that looks like the following.
<DataSet>
<xs:schema id="DataSet" xmlns=""
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xs:element name="DataSet" msdata:IsDataSet="true">
<xs:complexType>
<xs:choice maxOccurs="unbounded">
<xs:element name="Users">
<xs:complexType>
<xs:sequence>
<xs:element name="UserID" msdata:DataType="System.Guid"
type="xs:string" minOccurs="0" />
<xs:element name="Name" type="xs:string" minOccurs="0" />
<xs:element name="UserName" type="xs:string" minOccurs="0" />
<xs:element name="PasswordHash" type="xs:string"
minOccurs="0" />
<xs:element name="DomainName" type="xs:string"
minOccurs="0" />
<xs:element name="EmailAddress" type="xs:string"
minOccurs="0" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>
<Users>
<UserID>30305200-83cb-11c5-8000-d455befdca22</UserID>
<Name>Some One</Name>
<UserName>someone</UserName>
<PasswordHash>fa6a602c680673f462cab11b2b07218e</PasswordHash>
<EmailAddress>someone@microsoft.com</EmailAddress>
</Users>
</DataSet>
It is a recommendation that you include the XML schema in the file because this schema helps validate the data. In this example (when all columns are of type string), it is not as important as when handling more complex data structures (which are normally the case in any real-world scenario). The data is the same as the data that the Windows Mobile Professional sample used, and you can create a DataSet object like this one with the following code.
using(SqlCeConnection cn = new SqlCeConnection(connectionString))
{
cn.Open();
SqlCeDataAdapter da = new SqlCeDataAdapter("SELECT * FROM Users", cn);
DataSet ds = new DataSet();
da.Fill(ds, "Users");
FileStream fs = File.OpenWrite(fileName);
XmlTextWriter xtw = new XmlTextWriter(fs, Encoding.UTF8);
ds.WriteXml(xtw, XmlWriteMode.WriteSchema);
xtw.Close();
fs.Close();
}
This is the code to use on the device, and you can use a very similar code on a desktop computer by removing all of the "Ce" phrases in the ADO.NET classes (SqlConnection, SqlDataAdapter, and so on). Also, the stream and writer objects are not required on the desktop computer.
With this file as a prerequisite, the Smartphone sample sets up the encrypted local storage by using the following code.
string applicationPath =
Path.GetDirectoryName(this.GetType().Assembly.GetName().CodeBase);
string localDataSetPath = applicationPath + Path.DirectorySeparatorChar +
"EncryptedDataSet.zip";
string dataSetPassword = "pe4eGaWR46a4e+UPR-c??&wa!uFu#asw";
string localDataSetXmlPath = applicationPath + Path.DirectorySeparatorChar +
"EncryptedDataSet.xml";
// Create only if it doesn't exist
if(File.Exists(localDataSetPath))
return;
// Load DataSet object from the text file
FileStream fs = File.OpenRead(localDataSetXmlPath);
XmlTextReader xtr = new XmlTextReader(fs);
DataSet ds = new DataSet();
ds.ReadXml(xtr, XmlReadMode.ReadSchema);
xtr.Close();
fs.Close();
// Save to an encrypted .zip file
ZipOutputStream zs = new ZipOutputStream(File.OpenWrite(localDataSetPath));
zs.Password = dataSetPassword;
zs.PutNextEntry(new ZipEntry(Path.GetFileName(localDataSetXmlPath)));
XmlTextWriter xtw = new XmlTextWriter(zs, Encoding.UTF8);
ds.WriteXml(xtw, XmlWriteMode.WriteSchema);
xtw.Close();
zs.Close();
ds = null;
First, the code retrieves the application path and uses it to create the full path of both the XML and .zip files. Then, the XML file is read into a DataSet object that creates the .zip file with the XML file as the only entry. The .zip file is also encrypted by using the same password that the Windows Mobile Professional sample used.
Even if this code was written to use the SharpZipLib library on the device, you could also create the .zip file on the desktop computer (or server) and distribute it with the application. It shouldn't be too difficult to find tools to create an encrypted .zip file that contains an XML file with the DataSet object; it can even be done using the Send To > Compressed (zipped) folder functionality in Windows Explorer). One advantage of using standards is the many tools that are available for different platforms.
The code to authenticate the user (when the user presses Login soft key) looks like the following.
// Get the MD5 hash from the user name and password that is provided
string userMD5 = GetMD5(userNameTextBox.Text + passwordTextBox.Text);
string applicationPath =
Path.GetDirectoryName(this.GetType().Assembly.GetName().CodeBase);
string localDataSetPath = applicationPath + Path.DirectorySeparatorChar +
"EncryptedDataSet.zip";
string dataSetPassword = "pe4eGaWR46a4e+UPR-c??&wa!uFu#asw";
// Load the DataSet object from the .zip file
ZipInputStream zs = new ZipInputStream(File.OpenRead(localDataSetPath));
zs.Password = dataSetPassword;
zs.GetNextEntry();
XmlTextReader xtr = new XmlTextReader(zs);
DataSet ds = new DataSet();
ds.ReadXml(xtr, XmlReadMode.ReadSchema);
xtr.Close();
zs.Close();
// Get the password hash from the DataSet object
string dbMD5 = string.Empty;
DataRow[] drs = ds.Tables["Users"].Select("UserName='" + userNameTextBox.Text + "'");
if(drs.Length == 1)
dbMD5 = drs[0]["PasswordHash"].ToString();
// Check password hash
if(userMD5 == dbMD5)
messageLabel.Text = "\r\nUser authorized!";
else
messageLabel.Text = "User NOT authorized!\r\nPlease try again.";
After the code calculates the password hash for the entered username and password, the code retrieves the stored hash value from the local database by using the .zip file's password. Reading from the .zip file is very similar to reading a file from the file system (see the previous code example). Finally, the code compares the two hash values and presents the result to the user in a label on the same form. Presenting the result on the same form improves the user experience compared to using a separate form (as would be the case if you use the MessageBox class).
The sample code to use Windows Authentication uses an XML Web service that implements a trivial method, as shown in the following code example.
[WebMethod]
public string HelloWorld(string name)
{
return "Hello " + name + "!";
}
After you create a Web reference to the XML Web service, the code to authenticate is identical to the code for Basic or Digest Authentication, as shown in the following code example.
try
{
WebServices.Service ws = new WebServices.Service();
ws.Credentials = new NetworkCredential(userNameTextBox.Text,
passwordTextBox.Text);
ws.PreAuthenticate = true;
string s = ws.HelloWorld(userNameTextBox.Text);
MessageBox.Show("User authorized!", this.Text);
MessageBox.Show(s);
}
catch(WebException ex)
{
HttpWebResponse resp = (HttpWebResponse)ex.Response;
if(resp.StatusCode == HttpStatusCode.Unauthorized)
MessageBox.Show("User NOT authorized!",this.Text);
else
throw;
}
When the virtual directory for the XML Web service in the Web server (IIS) has (Integrated) Windows Authentication enabled, the authentication manager uses the custom authentication module to respond to the authentication challenge that the server sends. You can avoid unnecessary network traffic by using preauthentication by setting the PreAuthenticate property to true.
For details about implementing the custom authentication module for Windows (NTLM) Authentication, see this article's download code sample.