Proper .NET content licensing can mean the difference between marketplace dominance and financial bankruptcy. And I’m just talking about trying to understand the license agreement that comes with Visual Studio. You still have to figure out a licensing method for your own application before you send it to your customers.
Licensing and license agreements are an essential means of protecting the intellectual property you’ve worked so hard to develop. How does licensing work? The key is found in the roots of the word itself: license comes from “li-” (to tell a lie) and “-cense” (from “cents” as in “pennies”). Together, these roots mean “to tell lies about small units of currency.” The confusion brought about in trying to figure out what this means keeps the bad guys perplexed and occupied long enough so that they don’t steal your application.
If this method doesn’t work, there are software solutions, some of which I’ll review in this chapter. Part of the discussion focuses on designing a licensing system that will appear in the Library Project. The .NET Framework does include classes for component licensing but they are primarily used for designers of controls used by other programmers within the Visual Studio IDE, and not for end-user applications. We will not be covering these licensing features in this chapter. If you’re curious about such features, start by reading about the License Compiler (lc.exe) in the Visual Studio online help.
Back in the early days of software, licensing wasn’t an issue: if you could get to the computer, it was because you were authorized. All user interaction with the system was through the programmers and technicians. If some user wanted to steal something, it would be in the form of 20 tons of steel, wires, and vacuum tubes. Fun? Yes. Easy? No.
Today, it’s a different story. Most users are non-technical, and some are unethical. So, now we have licensing agreements and teams of lawyers to back it all up. But we also have software, software that can delicately enforce some of the rules. For a particular piece of software, there is still the question of “How much licensing enforcement code do I add to my application?” The amount of software control you include will fall somewhere in the “Freedom-Security” continuum shown in Figure 22-1.
If you go for the Freedom end of the spectrum (“convenient for users and hackers”), you will have to go on the trustworthiness of your users, and any armed guards you have dispatched to their offices, to keep the program in compliance. At the Security end of the scale (“secure for programmers and highly paid law firms”), the software implements practices and policies that ensure that only licensed users of the application ever use or install it; no armed guards needed.
The rest of this section discusses some possible options you could choose within the Freedom-Security range.
The license agreement-only method clearly opts for freedom over security. When you supply the user with software, it comes with a carefully crafted license agreement that lays out the terms of use for both the user and the software supplier. It generally gives the user certain rights as to installation, use, and distribution of the software.
When you write an application for use only within a specific organization or by a small group of users who you will have regular contact with, the license agreement-only method may be just what you need. In fact, I would bet that most Visual Basic applications are in this vein. Microsoft has announced over the years that the vast majority of Visual Basic programmers target their applications for use in a specific business organization, tied to a specific custom database. Such systems often require very little in the way of license enforcement, since the application is useless when carried outside the building where it was meant to reside.
Even if your software achieves widespread distribution, this licensing scheme may still be the way to go. Many open source applications, including a major operating system that rhymes with “Plinux,” use the Free Software Foundation’s GNU General Public License (http://www.fsf.org/licensing/licenses/gpl.html) as their primary licensing and distribution policy.
If you need a bit more control over the distribution, installation, and use of an application, you can impose a generated general license key—basically a password that allows the application to be installed or used. Such keys are often entered at the start of the installation process, with the user prompted for a specific key. Without the key, it’s goodbye installation.
The software vendor will need a way to generate a good set of unique installation keys. There are a couple of options:
Just generate a sequential serial number, and mix into it a product ID and version number. The great thing about such a key is that it is easy to generate. The installation program doesn’t need to perform any complex verification logic on the key. It only needs to ensure that the general format is correct. One of the products I used to develop online help documentation for my older Visual Basic 6.0 applications used such a license key. In a way, it’s not much more secure than using just a license agreement, since anyone who knows the general format can make up his own key.
Use a hashed or scrambled key, based on some original serial number or formula that can be verified by the installation program. A well-crafted hashing algorithm can generate a wide range of keys, but makes it difficult for others who don’t know the formula to generate their own fake keys. Although I am not privy to Microsoft’s internal processes, this appears to be the method it uses for its 25-character “CD Keys,” including the one supplied with Visual Studio. Although it is difficult for keys to be invented out of whole cloth, the public nature of the keys makes them subject to sharing. For some of its products, Microsoft combines a CD key with an online or phone-based registration process to enhance security.
Supply a hashed or encrypted key based on a serial number that is (secretly) supplied with the installation program or distribution media. When the user enters the key, it is unencrypted or otherwise prepared, and then compared with the serial number. Only if it matches will the software installation complete properly.
A custom-generated license key is similar to a general generated key, but uses personal information supplied by the user as part of the generation process. Such a key is more interactive, and requires that the end-user specifically communicate with the software vendor (or an application on its web site) to complete the installation process.
During the purchase or installation process, the user makes specific information (such as the owner’s name and the date of purchase) available to the software vendor. The vendor then uses public-private key encryption (asymmetric cryptography) to either fully encrypt or digitally sign the relevant information. The encrypted signature is then returned to the end-user for installation. The installation process uses the public portion of the key pair to ensure that the signature is valid.
We will use this license key method in the Library Project, so I’ll have more to say about it a little later.
For paranoid software vendors, or for those who have a legitimate need to keep a tight rein on their installation base, there are solutions that involve regular access to hardware or services to confirm that previously installed software is legal and valid. One popular method uses a “dongle,” typically a USB port-based device that the software must have access to each time it runs. The software vendor supplies a dongle with the licensed software, and may encode it with date-based or use-based limits.
With the prevalence of the Internet, software vendors also have the option of real-time verification over the Web. Each time the program runs, it could access a known vendor site to engage in a usage verification process. Such a system allows for ongoing monitoring of the software by vendors who may have a business or governmental reason to limit use of the software.
For one of my customer projects, I must access a third-party web site on a monthly basis and download proprietary data for use with that vendor’s software. The vendor requires that I always access their web site from a specific machine with a specific IP address. It will refuse to supply the data if I attempt to connect from any other machine. If I have a real need to use a fresh IP address (if, for example, I change Internet service providers), I must submit paperwork to the vendor informing them of the new IP address. It seems pesky, and it is an irritation. But the data they supply is unique and valuable, and they feel they have a business need to protect that investment. Since my customer requires the data, I have no choice but to comply with the monthly verification procedures.
The highest level of security requires a blatant distrust of the user, although there may be good reason for this. For highly sensitive applications, the software vendor may make their product available to only a limited number of customers, and then only on a lease basis. As part of the lease agreement, the customer agrees to have a trained staff member of the software vendor on-site, running and maintaining the application for the customer. At the very least, the vendor will require that one of its employees be immediately available to the customer whenever the application is used.
In a world of off-the-shelf software applications, it seems unconscionable that such a system could exist. But in high-risk situations, security concerns are raised to such a level that neither party is willing to fully assume the risks of installing and using the application apart from the other.
Although I was tempted to use this system for the Library Project, I think we’ll stick with our original plan of employing a custom-generated license key.
A license agreement is a document wherein the party of the first part hereby and does amicably render to the party of the second part certain rights, quid pro quos, treasury bonds, and other benefits; in exchange, the party of the second part will do the same for the party of the first part without respect for any other party or festival.
Let’s try that again. A license agreement tells a user “Go ahead, install and use the software, but you have to follow these rules.” Although they are often written in legalese, they can also appear in a real language, such as English. They also range in granted rights, from “You can use this, but when you’re finished, you must destroy all copies” to “Use it, and feel free to pass a copy of the program and its source code to your friends and relations.”
The Library software provided with this book comes with a license agreement. (I’ve included it in Appendix B.) When you installed the sample code, you agreed to the terms of the license agreement, including the part about supporting my family financially well into my retirement years. But enough about me; let’s talk about license agreements you may want to use for your applications.
If you’re developing a DVD catalog program for your cousin Fred, you can probably skip the license agreement part. But any software you craft in a business capacity for use outside your own company should include some sort of agreement between you (or your company) and the user of the software. This agreement could be defined as part of the contract that established the software development project (this is typical for software consulting), or you could include the agreement as a component of the software (common for off-the-shelf programs).
Whichever method you choose, it is important that you state it in written form because it can save you grief down the road. I once had a customer who insisted that I fork over a copy of the source code for an application I wrote for them so that they could enhance it and sell the new version to other businesses (the nerve!). Fortunately, we had a written contract that stated the rules of engagement. They were entitled to a copy of the source code for archive purposes, but they could not use it or derive products from it without written consent from me. This granted a level of safety for them while still providing the means for me to provide the best support possible for their organization. Fortunately, it all came to a happy conclusion, and since that Visual Basic 3.0 code doesn’t even run anymore, it’s a moot point.
A license agreement usually exists to protect the rights of the software vendor, but it would be useless if it didn’t also grant meaningful rights to the user—and some of the rights can be rather generous. Did you know that the standard consumer licensing agreement for Microsoft Office allows you to install the product on two different systems using a single licensed copy of the program? It’s not a complete install-fest. Both computers must belong to the same person, and one must be a desktop whereas the other is a portable device (a laptop). But it’s still a meaningful benefit to the typical user.
The legal department at O’Reilly Media wants to remind you that Tim Patrick does not have a sufficient understanding of the law, and cannot advise you on the contents of any licensing agreement you may want to craft for your projects.
I hinted a little about the obfuscation features in Visual Studio 2008 in Chapter 1 and Chapter 5, but it’s high time we actually took a look at the features. Visual Studio includes a stripped-down version of Dotfuscator from a company named PreEmptive Solutions (not a part of Microsoft—yet). To access the program, use the Tools → Dotfuscator Community Edition menu command in Visual Studio. The main interface appears in Figure 22-2.
As of this writing, Dotfuscator Community Edition is not included with Visual Basic 2008 Express Edition.
Even though this is the basic version of the product, you can see that it has a gazillion options. If you want to dive into its enhanced features for your project, that’s fantastic. I’ll just cover the basic usage here.
Let’s recall quickly why you would want to obfuscate your code, or even use the word obfuscate in mixed company. Here’s some code from the Library Project:
Public Function CenterText(ByVal origText As String, _ ByVal textWidth As Integer) As String ' ----- Center a piece of text in a field width. ' If the text is too wide, truncate it. Dim resultText As String resultText = Trim(origText) If (Len(resultText) >= textWidth) Then ' ----- Truncate as needed. Return Trim(Left(origText, textWidth)) Else ' ----- Start with extra spaces. Return Space((textWidth - Len(origText)) 2) & _ resultText End If End Function
This code is quite easy to understand, especially with the comments and the meaningful method and variable names. Although .NET obfuscation works at the MSIL level, let’s pretend that the obfuscator worked directly on Visual Basic code. Obfuscation of this code might produce results similar to the following:
Public Function A(ByVal AA As String, _ ByVal AAA As Integer) As String Dim AAAA As String AAAA = Trim(AA) If (Len(AAAA) >= AAA) Then Return Trim(Left(AA, AAA)) Else Return Space((AAA - Len(AA)) 2) & AAAA End If End Function
In such a simple routine, we could still figure out the logic, but with more effort than in the original version. Naturally, true obfuscation goes much further than this, scrambling the readability of the code at the IL level, and confounding code readers and hackers alike.
To obfuscate an assembly:
Build your project in Visual Studio using the Build → Build [Project Name] menu command.
Start Dotfuscator using the Tools → Dotfuscator Community Edition menu command in Visual Studio.
When prompted for a project type, select Create New Project, and click the OK button.
On the Input tab of the Dotfuscator application window, click the “Browse and add assembly to list” toolbar button. This is the leftmost button—the one that looks like a file folder with a small arrow above it—on the panel shown in Figure 22-2.
When prompted for an assembly file, browse for your compiled application, and click the OK button. The assembly to use will be in the binRelease subdirectory within your project’s source code directory.
Select the File → Build menu command to generate the obfuscated assembly. You will be prompted to save the Dotfuscator project file (an XML file) before the build begins. Save this file to a new directory. When the build occurs, it will save the output assembly in a Dotfuscated subdirectory in the same directory that contains the XML project file.
The build completes, and a summary appears as shown in Figure 22-3. Your obfuscated file is ready to use. The process also generates a Map.xml file that documents all the name changes made to types and members within your application. It would be a bad thing to distribute this file with the assembly. It is for your debugging use only.
To prove that the obfuscation took place, use the IL Disassembler tool that comes with Visual Studio to examine each assembly. (On my system, this program is accessed via Start → [All] Programs → Microsoft Windows SDK v6.0A → Tools → IL Disassembler.) Figure 22-4 shows the global variables included in the Library Project’s General.vb file. The obfuscated version of these same variables appears in Figure 22-5.
I will not be performing obfuscation on the Library Project through this book’s tutorial sections. Feel free to try it out on your own.
The tools and procedures we will use to design the Library Project’s licensing system can be built from featuresMapMap.xml file.xml file alreaMap.xml filedy Map.xml filediscussed in previous chapters:
The license file contains XML content. (Chapter 13)
The license appears as a separate file in the same directory as the Library.exe assembly. The Library software reads content from the license file. (Chapter 15)
The license will include a digital signature, which is based on public-private key encryption. (Chapter 11)
Each time the Library application runs, it attempts to read the license file. If the file doesn’t exist, or if it contains invalid data or an invalid signature, the program downgrades its available features, disabling those features that are considered licensed.
The Library Project’s license file contains some basic ownership and rights information related to the user who purchased rights to the software. Here’s the XML content I’ve come up with:
<?xml version="1.0" encoding="utf-8"?> <License> <Product>Library Project</Product> <LicenseDate>1/1/2000</LicenseDate> <ExpireDate>12/31/2999</ExpireDate> <CoveredVersion>1.*</CoveredVersion> <Licensee>John Q. Public</Licensee> <SerialNumber>LIB-123456789</SerialNumber> </License>
That seems sufficient. The process that builds the digital signature also stores an encrypted signature within the XML content.
In the "Project" section of this chapter, we’ll build a new application that exists solely to generate license files for the Library application. It will have three primary components:
Generate and manage the public and private keys used in the signature process.
Prompt the user for the license date, expiration date, covered version, licensee name, and serial number for a single license. These are the values that appear in the license file’s XML content.
Output the XML license file and digitally sign it using the private key.
The "Project" section of this chapter will show you how to generate a generic license file. This XML file will be distributed and installed with the Library application using the setup program that we will build in Chapter 25. The file will be named LibraryLicense.lic (by default) and will always appear in the same directory as the Library.exe application file.
If I were developing a real application for paying customers, and I had a web site that supported a web service (which I’ll talk about in Chapter 23), here is one design for installing the license file that I might use:
Run the setup program to install the application on the user’s workstation.
During installation, the setup program prompts the user for the license details that will ultimately appear in the XML license file.
The setup program contacts the web service on my vendor web site, and passes the user-supplied values to that registration service.
The registration service returns a digitally signed XML file that contains the licensing content.
The setup program installs this file along with the application.
If for any reason the licensing cannot complete successfully during setup, the main application contains identical licensing code, and can communicate with the registration service itself.
Whenever the Library application runs, it reads in the XML license file and performs many checks to ensure that the license is valid for the current application installation. If the license is invalid for any reason, the application blocks access to the enhanced administrative features included in the Library system.
Since you will often spend dozens or hundreds of hours designing and developing a quality Visual Basic application, it is important to use appropriate licensing and obfuscation technology to protect your hard work. Licensing is another one of those common programming tasks that didn’t make it into the .NET Framework as an easy-to-use class—unless you are building and distributing design-time controls. For the rest of us, it’s make-it-up-as-you-go time. Fortunately, .NET has great support tools, so adding licensing support isn’t too difficult.
In this chapter’s project code, we’ll follow two of the four licensing steps discussed in the section "The Library Licensing System,” earlier in this chapter: generating the license file and using the license file. The design we created previously is good enough for our needs, although we still need to record it in the project’s technical documentation. We won’t formally install the license file until we create the setup program in Chapter 25.
Since we’ll be adding a new external file that will be processed by the Library Project, we need to document its structure in the project’s Technical Resource Kit. Let’s add the following new section to that document.
The Library Project reads a customer-specific license file generated by the Library License Generation support application. That program generates a digitally signed XML license file that includes licensee information. Here is a sample of the license file content:
<?xml version="1.0"?>
<License>
<Product>Library Project</Product>
<LicenseDate>1/1/2000</LicenseDate>
<ExpireDate>12/31/2999</ExpireDate>
<CoveredVersion>1.*</CoveredVersion>
<Licensee>John Q. Public</Licensee>
<SerialNumber>LIB-123456789</SerialNumber>
<Signature>
Digital signature appears here (not shown)
</Signature>
</License>
The <LicenseDate>
and <ExpireDate>
tags indicate the first and last valid dates of the license. <Licensee>
indicates the name of the license owner. <SerialNumber>
includes the vendor-defined serial number associated with this license. The <CoveredVersion>
tag contains data similar to the assembly version number included in .NET applications. It has up to four dot-delimited parts:
<major>.<minor>.<build>.<revision>
Each component can include a number from 0 to 9999, or the *
character, which indicates all valid values for that position.
The <Signature>
section contains the generated digital signature. Its format is dependant on the XML Cryptography tools in .NET that generate this section. To ensure a proper digital signature, always use the Library License Generation support application to build license files.
That support application generates a public and private key pair for use in digital signing. The public portion of this key (as an XML file) must be added as a resource named LicensePublicKey
to the Library application. The private portion must be kept private. For consistency, the same key pair should be used throughout the lifetime of the Library Project’s availability.
We will also store the location of the license file as an application setting in the main program. We need to record that setting with the other application settings already added to the User Settings section of the Resource Kit.
LicenseFileLocation
The path to the Library License file on this workstation. If not supplied, the program will look for a file named LibraryLicense.lic in the same folder as the application.
Generating license files and digital signatures by hand using Notepad would be . . . well, let’s not even think about it. Instead, we’ll depend on a custom application to create the files and signatures for us. I’ve already developed that custom tool for you. You’ll find it in the installation directory for this book’s code, in the LibraryLicensing subdirectory.
This support application includes two main forms. The first (KeyLocationForm.vb, shown in Figure 22-6) locates or creates the public-private key files used in the digital signature process.
Most of the form’s code helps to locate and verify the folder that will contain the two key files (one private, one public). Some of the code in the ActGenerate_Click
event handler creates the actual files.
Dim twoPartKey As RSA Dim publicFile As String Dim privateFile As String ...some code skipped here, then... ' ----- Generate the keys. twoPartKey = New RSACryptoServiceProvider twoPartKey = RSA.Create( ) ' ----- Save the public key. My.Computer.FileSystem.WriteAllText(publicFile, _ twoPartKey.ToXmlString(False), False) ' ----- Save the private key. My.Computer.FileSystem.WriteAllText(privateFile, _ twoPartKey.ToXmlString(True), False)
That’s really simple! The System.Security.Cryptography.RSA
class and the related RSACryptoServiceProvider
class do all the work. All you have to do is call the RSA.Create
method, and then generate the relevant XML keys using the ToXmlString
method, passing an argument of False
for the public key and True
for the private key. If you want to look at some sample keys, open the LicenseFiles subdirectory in this book’s source installation directory. You’ll find two files, one for the public key and one for the private key. I’d print one of them here, but it all just looks like random characters.
The other support form is MainForm.vb, which generates the actual end-user license file, and appears in Figure 22-7.
As with the first form, most of this form’s code simply ensures that the public and private key files are intact, and that the user entered valid data before generation. The ActGenerate_Click
event handler is where the real fun is. First, we need some XML content, which we build in the BuildXmlLicenseContent
method. It creates the content element by element, using the methods we learned about in Chapter 13. For instance, here’s the part of the code that adds the serial number:
dataElement = result.CreateElement("SerialNumber") dataElement.InnerText = Trim(SerialNumber.Text) rootElement.AppendChild(dataElement)
Then comes the digital signature, via the SignXmlLicenseContent
function, most of which appears here:
Private Function SignXmlLicenseContent( _ ByVal sourceXML As XmlDocument) As Boolean ' ----- Add a digital signature to an XML document. Dim privateKeyFile As String Dim privateKey As RSA Dim signature As SignedXml Dim referenceMethod As Reference ' ----- Load in the private key. privateKeyFile = My.Computer.FileSystem.CombinePath( _ KeyLocation.Text, PrivateKeyFilename) privateKey = RSA.Create( ) privateKey.FromXmlString( _ My.Computer.FileSystem.ReadAllText(privateKeyFile)) ' ----- Create the object that generates the signature. signature = New SignedXml(sourceXML) signature.SignedInfo.CanonicalizationMethod = _ SignedXml.XmlDsigCanonicalizationUrl signature.SigningKey = privateKey ' ----- The signature will appear as a <reference> ' element in the XML. referenceMethod = New Reference("") referenceMethod.AddTransform(New _ XmlDsigEnvelopedSignatureTransform(False)) signature.AddReference(referenceMethod) ' ----- Add the signature to the XML content. signature.ComputeSignature( ) sourceXML.DocumentElement.AppendChild(signature.GetXml( )) ' ----- Finished. Return True End Function
Digital signing occurs via the SignedXml
class (in the System.Security.Cryptography.Xml
namespace). This class uses a few different signing methods; the one I chose (XmlDsigCanonicalizationUrl
) is used for typical XML and ignores embedded comments.
This signature appears as tags and values in the XML output, added through the AppendChild
statement near the end of the routine. Since we don’t want the signature itself to be considered when we later scan the XML file for valid content, the SignedXml
class adds the signature as a <reference>
tag. This occurs in code by adding a Reference
object that is programmed for that purpose. It’s added through the signature.AddReference
method call.
Once we have the signature in the XML content, we write it all out to a file specified by the user via the standard XmlDocument.Save
method (in the ActGenerate_Click
event handler).
licenseXML.Save(LicenseSaveLocation.FileName)
Here’s a sample XML license file that includes a digital signature. This is the one that I have included in the LicenseFiles directory in the book’s source installation directory (with some lines wrapped to fit this page).
<?xml version="1.0"?> <License> <Product>Library Project</Product> <LicenseDate>1/1/2000</LicenseDate> <ExpireDate>12/31/2999</ExpireDate> <CoveredVersion>1.*</CoveredVersion> <Licensee>John Q. Public</Licensee> <SerialNumber>LIB-123456789</SerialNumber> <Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> <SignedInfo> <CanonicalizationMethod Algorithm= "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" /> <SignatureMethod Algorithm= "http://www.w3.org/2000/09/xmldsig#rsa-sha1" /> <Reference URI=""> <Transforms> <Transform Algorithm="http://www.w3.org/2000/09/ xmldsig#enveloped-signature" /> </Transforms> <DigestMethod Algorithm="http://www.w3.org/2000/09/ xmldsig#sha1" /> <DigestValue>Dn6JYIBI/qQudmvSiMvuOvnVBGU= </DigestValue> </Reference> </SignedInfo> <SignatureValue>NULghI4WbzDLroIcf2u9aoybfSjXPJRN5 0UMrCPYa5bup+c7RJnqTM+SzP4jmfJWPPs7pOvDC/fbdNY VMaoyXW0jL3Lk8du3X4JXpW3xp9Nxq31y/Ld8E+RkoiPO6 KRGDI+RRZ8MAQda8WS+L2fMyenRAjo+fR9KL3sQ/hOfQX8= </SignatureValue> </Signature> </License>
The digital signature appears as the scrambled content within the <SignatureValue>
tag. Now, if anyone tries to modify any of the license values, the license will no longer match the signature, and the entire license will become invalid.
Instead of using a digital signature, we could have just encrypted the entire licensing file with the private key, and later used the public key to decrypt it and examine its contents. But I like the digital signature better, since it allows anyone to open the license file and check the parameters of the license itself while still preventing any changes.
Let’s return to the Library application already in progress.
Load the Chapter 22 (Before) Code project, either through the New Project templates or by accessing the project directly from the installation directory. To see the code in its final form, load Chapter 22 (After) Code instead.
The program will adjust its behavior depending on whether it is licensed or not. But to make that determination, it needs to ensure that the contents of the licensing file are valid and haven’t been tampered with. To do this, it needs a way to unscramble the signature and compare it with the rest of the license to make sure it matches. We built the signature using the private key; we must unscramble it using the public key.
We could store the public key in its own file outside the program, but then it might get lost (just like my real keys). Instead, we’ll store the public key as an application resource, found externally in the source code’s Resources folder. I’ve already added the resource to your copy of the program, and named it LicensePublicKey
. With this key embedded in the application, any regeneration of the public and private keys will require modification of this resource. In code, we refer to the XML content of the public key using its resource name:
My.Resources.LicensePublicKey
Some of the security features use classes found in the System.Security.Cryptography.Xml
namespace. This is not one of the namespaces included by default in new Visual Basic applications, so we’ll have to add it ourselves. Open the project properties window and select the References tab. Just below the list of References, click the Add button, and then select System.Security from the .NET tab of the Add Reference window that appears.
Since we have the project properties window still open, click over to the Settings tab. Add a new String setting and use “LicenseFileLocation” for its name. We’ll use this setting to store the path to the license file. Save and close the project properties window.
Our general licensing needs throughout the application are pretty simple. We only need to know the current status of the licensing file, and have access to a few of the licensing values so that we can display a short message about the license. We may need to do this in various parts of the program, so let’s add some useful generic code to the General.vb module. Open that module now.
Right at the top of that file, the code already includes a reference to the System.Security.Cryptography
namespace, since we include code that encrypts user passwords. But this doesn’t cover the standard or secure XML stuff. So add two new Imports
statements as well.
Insert Chapter 22, Snippet Item 1.
Imports System.Xml Imports System.Security.Cryptography.Xml
We’ll use an enumeration to indicate the status of the license. Add it now to the General
module.
Insert Chapter 22, Snippet Item 2.
Public Enum LicenseStatus ValidLicense MissingLicenseFile CorruptLicenseFile InvalidSignature NotYetLicensed LicenseExpired VersionMismatch End Enum
Let’s also add a simple structure that communicates the values extracted from the license file. Add this code to the General
module.
Insert Chapter 22, Snippet Item 3.
Public Structure LicenseFileDetail Public Status As LicenseStatus Public Licensee As String Public LicenseDate As Date Public ExpireDate As Date Public CoveredVersion As String Public SerialNumber As String End Structure
By default, the license file appears in the same directory as the application, using the name LibraryLicense.lic. Add a global constant to the General
module that identifies this default name.
Insert Chapter 22, Snippet Item 4.
Public Const DefaultLicenseFile _ As String = "LibraryLicense.lic"
All we need now is some code to fill in the LicenseFileDetail
structure. Add the new ExamineLicense
function to the General
module.
Insert Chapter 22, Snippet Item 5.
Public Function ExamineLicense( ) As LicenseFileDetail ' ----- Examine the application's license file, and ' report back what's inside. Dim result As New LicenseFileDetail Dim usePath As String Dim licenseContent As XmlDocument Dim publicKey As RSA Dim signedDocument As SignedXml Dim matchingNodes As XmlNodeList Dim versionParts( ) As String Dim counter As Integer Dim comparePart As String ' ----- See if the license file exists. result.Status = LicenseStatus.MissingLicenseFile usePath = My.Settings.LicenseFileLocation If (usePath = "") Then usePath = _ My.Computer.FileSystem.CombinePath( _ My.Application.Info.DirectoryPath, DefaultLicenseFile) If (My.Computer.FileSystem.FileExists(usePath) = False) _ Then Return result ' ----- Try to read in the file. result.Status = LicenseStatus.CorruptLicenseFile Try licenseContent = New XmlDocument( ) licenseContent.Load(usePath) Catch ex As Exception ' ----- Silent error. Return result End Try ' ----- Prepare the public key resource for use. publicKey = RSA.Create( ) publicKey.FromXmlString(My.Resources.LicensePublicKey) ' ----- Confirm the digital signature. Try signedDocument = New SignedXml(licenseContent) matchingNodes = licenseContent.GetElementsByTagName( _ "Signature") signedDocument.LoadXml(CType(matchingNodes(0), _ XmlElement)) Catch ex As Exception ' ----- Still a corrupted document. Return result End Try If (signedDocument.CheckSignature(publicKey) = False) Then result.Status = LicenseStatus.InvalidSignature Return result End If ' ----- The license file is valid. Extract its members. Try ' ----- Get the licensee name. matchingNodes = licenseContent.GetElementsByTagName( _ "Licensee") result.Licensee = matchingNodes(0).InnerText ' ----- Get the license date. matchingNodes = licenseContent.GetElementsByTagName( _ "LicenseDate") result.LicenseDate = CDate(matchingNodes(0).InnerText) ' ----- Get the expiration date. matchingNodes = licenseContent.GetElementsByTagName( _ "ExpireDate") result.ExpireDate = CDate(matchingNodes(0).InnerText) ' ----- Get the version number. matchingNodes = licenseContent.GetElementsByTagName( _ "CoveredVersion") result.CoveredVersion = matchingNodes(0).InnerText ' ----- Get the serial number. matchingNodes = licenseContent.GetElementsByTagName( _ "SerialNumber") result.SerialNumber = matchingNodes(0).InnerText Catch ex As Exception ' ----- Still a corrupted document. Return result End Try ' ----- Check for out-of-range dates. If (result.LicenseDate > Today) Then result.Status = LicenseStatus.NotYetLicensed Return result End If If (result.ExpireDate < Today) Then result.Status = LicenseStatus.LicenseExpired Return result End If ' ----- Check the version. versionParts = Split(result.CoveredVersion, ".") For counter = 0 To UBound(versionParts) If (IsNumeric(versionParts(counter)) = True) Then ' ----- The version format is ' major.minor.build.revision. Select Case counter Case 0 : comparePart = _ CStr(My.Application.Info.Version.Major) Case 1 : comparePart = _ CStr(My.Application.Info.Version.Minor) Case 2 : comparePart = _ CStr(My.Application.Info.Version.Build) Case 3 : comparePart = _ CStr(My.Application.Info.Version.Revision) Case Else ' ----- Corrupt version number. Return result End Select If (Val(comparePart) <> _ Val(versionParts(counter))) Then result.Status = LicenseStatus.VersionMismatch Return result End If End If Next counter ' ----- Everything seems to be in order. result.Status = LicenseStatus.ValidLicense Return result End Function
That’s a lot of code, but most of it just loads and extracts values from the XML license file. The signature-checking part is relatively short.
publicKey = RSA.Create( ) publicKey.FromXmlString(My.Resources.LicensePublicKey) signedDocument = New SignedXml(licenseContent) matchingNodes = licenseContent.GetElementsByTagName( _ "Signature") signedDocument.LoadXml(CType(matchingNodes(0), XmlElement)) If (signedDocument.CheckSignature(publicKey) = False) Then ' ----- Invalid End If
The SignedXml
object—which we also used to generate the original license file—needs to know exactly which XML tag in its content represents the digital signature. You would think that having an element named <Signature>
would be a big tip-off, but perhaps not. Anyway, once you’ve assigned that node using the SignedXml.LoadXml
method, you call the CheckSignature
method, passing it the public key. If it returns True
, you’re good. I mean, not in a moral sense; the code doesn’t know anything about you. But the signature is good.
When we added the About
form to the project a few hundred pages ago, we included a Label
control named LabelLicensed
. It currently always displays “Unlicensed,” but now we have the tools to display a proper license, if available. Open the source code for the About.vb form, and add the following code to the start of the AboutProgram_Load
event handler.
Insert Chapter 22, Snippet Item 6.
' ----- Prepare the form. Dim licenseDetails As LicenseFileDetail ' ----- Display the licensee. licenseDetails = ExamineLicense( ) If (licenseDetails.Status = LicenseStatus.ValidLicense) Then LabelLicensed.Text = _ "Licensed to " & licenseDetails.Licensee & vbCrLf & _ "Serial number " & licenseDetails.SerialNumber End If
Figure 22-8 shows the About
form in use with details displayed from the license file.
Just for fun, I changed the version number in my license file from “1.*” to “2.*” without updating the digital signature. Sure enough, when I displayed the About
form again, it displayed “Unlicensed,” since the check of the signature failed. How did I test the code this early? I copied the LibraryLicense.lic file from the book’s installed LicenseFiles subdirectory and placed that copy in the binDebug subdirectory of the project’s source code. Later on, you’ll be able to put the file anywhere you want and browse for it, but we’re getting ahead of ourselves.
At some point, a missing or invalid license should have a negative impact on the use of the application. When that happens, we should give the user a chance to correct the problem by locating a valid license file. We’ll do this through the new LocateLicense.vb form. I’ve already added the form to your project. It appears in Figure 22-9.
This form starts up with a call to its public ChangeLicense
function, which returns True
if the user changes the license. Most of this form’s code manages the display, presenting detailed reasons why a license is valid or invalid using the results of the ExamineLicense
function. If for any reason the license is invalid, a click on the Locate button lets the user browse for a better version.
Private Sub ActLocate_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles ActLocate.Click ' ----- Prompt the user for a new license file. If (LocateLicenseDialog.ShowDialog( ) <> _ Windows.Forms.DialogResult.OK) Then Return ' ----- Store the new path. My.Settings.LicenseFileLocation = _ LocateLicenseDialog.FileName LocationModified = True ' ----- Update the display. DisplayLicenseStatus( ) LicensePath.Text = My.Settings.LicenseFileLocation End Sub
The LocationModified
form-level variable gets sent back to the caller as a trigger to refresh the status of the license.
For the Library Project in particular, I didn’t see a point in enforcing the license on startup, since it’s not the patrons’ fault that the library stole this important work of software. Instead, I delay the verification process until an administrator or librarian tries to access the enhanced features of the application. Then, if the license check fails, the user should be able to browse the disk for a valid license file.
I think the best place to add the license check is just after the administrator successfully supplies a password. If we checked before that point, it would give ordinary patrons the ability to browse the disk, which is probably a no-no, since anyone and her uncle can walk up and use a patron workstation. Open the source code for the ChangeUser.vb form, locate the ActOK_Click
event handler, and locate the “Successful login” comment.
' ----- Successful login. LoggedInUserID = CInt(dbInfo!ID) LoggedInUserName = CStr(dbInfo!LoginID) ...
Just before this block of code, add the following license-checking code.
Insert Chapter 22, Snippet Item 7.
' ----- Don't allow the login if the program is unlicensed. Do While (ExamineLicense( ).Status <> _ LicenseStatus.ValidLicense) ' ----- Ask the user what to do. If (MsgBox("This application is not properly licensed " & _ "for administrative use. If you have access to " & _ "a valid license file, you can verify it now. " & _ "Would you like to locate a valid license file " & _ "at this time?", MsgBoxStyle.YesNo Or _ MsgBoxStyle.Question, ProgramTitle) <> _ MsgBoxResult.Yes) Then dbInfo.Close( ) dbInfo = Nothing Return End If ' ----- Prompt for an updated license. Call LocateLicense.ChangeLicense( ) LocateLicense = Nothing Loop
This code gives the user an unlimited number of chances to locate a valid license file. Once the license is validated, the code moves forward and enables administrative access.
The last major set of code to be added to the Library Project isn’t related to licensing, but it’s important nonetheless: the processing of fines for overdue items. We’ll add a common method that will perform the processing, and then call it where needed throughout the application.
Add the new DailyProcessByPatronCopy
method to the General
module.
Insert Chapter 22, Snippet Item 8.
Public Sub DailyProcessByPatronCopy( _ ByVal patronCopyID As Integer, ByVal untilDate As Date) ' ----- This routine does the most basic work of ' processing overdue fines. All other daily ' processing routines eventually call this routine. Dim sqlText As String Dim dbInfo As SqlClient.SqlDataReader Dim daysToFine As Integer Dim lastProcess As Date Dim fineSoFar As Decimal On Error GoTo ErrorHandler ' ----- Get all of the basic values needed to process ' this entry. sqlText = "SELECT PC.DueDate, PC.ProcessDate, " & _ "PC.Fine, CMT.DailyFine FROM PatronCopy AS PC " & _ "INNER JOIN ItemCopy AS IC ON PC.ItemCopy = IC.ID " & _ "INNER JOIN NamedItem AS NI ON IC.ItemID = NI.ID " & _ "INNER JOIN CodeMediaType AS CMT ON " & _ "NI.MediaType = CMT.ID " & _ "WHERE PC.ID = " & patronCopyID & _ " AND PC.DueDate <= " & DBDate(Today) & _ " AND PC.Returned = 0 AND PC.Missing = 0 " & _ "AND IC.Missing = 0" dbInfo = CreateReader(sqlText) If (dbInfo.Read = False) Then ' ----- Missing the patron copy record. Oh well. ' It was probably because this item was not ' yet overdue, or it was missing, or something ' valid like that where fines should not increase. dbInfo.Close( ) dbInfo = Nothing Return End If ' ----- If we have already processed this record for today, ' don't do it again. If (IsDBNull(dbInfo!ProcessDate) = False) Then If (CDate(dbInfo!ProcessDate) >= untilDate) Then dbInfo.Close( ) dbInfo = Nothing Return End If lastProcess = CDate(dbInfo!ProcessDate) Else lastProcess = CDate(dbInfo!DueDate) End If ' ----- Fines are due on this record. Figure out how much. daysToFine = CInt(DateDiff(DateInterval.Day, _ CDate(dbInfo!DueDate), untilDate) - _ DateDiff(DateInterval.Day, CDate(dbInfo!DueDate), _ lastProcess) - FineGraceDays) If (daysToFine < 0) Then daysToFine = 0 fineSoFar = 0@ If (IsDBNull(dbInfo!Fine) = False) Then _ fineSoFar = CDec(dbInfo!Fine) fineSoFar += CDec(dbInfo!DailyFine) * CDec(daysToFine) dbInfo.Close( ) dbInfo = Nothing ' ----- Update the record with the latest fine and ' processing information. sqlText = "UPDATE PatronCopy SET " & _ "ProcessDate = " & DBDate(untilDate) & _ ", Fine = " & Format(fineSoFar, "0.00") & _ " WHERE ID = " & patronCopyID ExecuteSQL(sqlText) Return ErrorHandler: GeneralError("DailyProcessByPatronCopy", Err.GetException( )) Resume Next End Sub
This code examines a PatronCopy
record—the record that marks the checking out of a single item by a patron—to see whether it is overdue, and if so, what penalty needs to be added to the record. Each record includes a ProcessDate
field. We don’t want to charge the patron twice on the same day for a single overdue item (no, we don’t), so we use the ProcessDate
to confirm which days are uncharged.
There are a few places throughout the application where we want to call this processing routine without bothering the user. The first appears in the PatronRecord
form, the form that displays the fines a patron still owes. Just before showing that list, we should refresh each item checked out by the patron to make sure we display the most up-to-date fine information. Open that form’s source code, locate the PatronRecord_Load
event handler, and add the following code, just before the call to RefreshPatronFines(-1)
that appears halfway through the routine.
Insert Chapter 22, Snippet Item 9.
' ----- Make sure that each item is up-to-date. For counter = 0 To ItemsOut.Items.Count - 1 newEntry = CType(ItemsOut.Items(counter), PatronDetailItem) DailyProcessByPatronCopy(newEntry.DetailID, Today) Next counter
The overdue status for an item must also be refreshed just before it is checked in. Open the source code for the MainForm
form and locate the ActDoCheckIn_Click
event handler. About halfway through its code, you’ll find a comment that starts with “Handle missing items.” Just before that comment, insert the following code.
Insert Chapter 22, Snippet Item 10.
' ----- Bring the status of the item up-to-date. DailyProcessByPatronCopy(patronCopyID, CheckInDate.Value)
Checkout needs to refresh the patron’s fines as well, just before letting the patron know whether there are, in fact, any fines due. Move to the MainForm.ActCheckOutPatron_Click
event handler, and add the following declarations to the top of the routine.
Insert Chapter 22, Snippet Item 11.
Dim dbTable As Data.DataTable Dim dbRow As Data.DataRow
In this same method, find a comment that starts with “Show the patron if there are any fines due.” As usual, it’s about halfway through the routine. Insert the following code just before that comment.
Insert Chapter 22, Snippet Item 12.
' ----- Bring the patron record up-to-date. sqlText = "SELECT ID FROM PatronCopy WHERE Returned = 0 " & _ "AND Missing = 0 AND DueDate < " & DBDate(Today) & _ " AND (ProcessDate IS NULL OR ProcessDate < " & _ DBDate(Today) & ") AND Patron = " & patronID dbTable = CreateDataTable(sqlText) For Each dbRow In dbTable.Rows DailyProcessByPatronCopy(CInt(dbRow!ID), Today) Next dbRow dbTable.Dispose( ) dbTable = Nothing
In addition to automatic fine processing, the Library Project also allows an administrator or librarian to perform daily processing of all patron items at will. This occurs through the Daily Processing panel on the main form (see Figure 22-10).
Currently, the panel doesn’t do much of anything, so let’s change that. The first task is to update the status label that appears at the top of the panel. Add a new method named RefreshProcessLocation
to the MainForm
form’s class.
Insert Chapter 22, Snippet Item 13.
I won’t show its code here, but it basically checks the CodeLocation.LastProcessing
database field either for all locations, or for the user-selected location, and updates the status display accordingly.
The user selects a location for processing with the ProcessLocation
drop-down list, but we haven’t yet added any code to populate that list. Find the TaskProcess
method in the main form’s source code, and add these declarations to the top of its code.
Insert Chapter 22, Snippet Item 14.
Dim sqlText As String Dim dbInfo As SqlClient.SqlDataReader On Error GoTo ErrorHandler
Then add these statements to the end of the method.
Insert Chapter 22, Snippet Item 15.
' ----- Refresh the list of locations. ProcessLocation.Items.Clear( ) ProcessLocation.Items.Add(New ListItemData( _ "<All Locations>", −1)) ProcessLocation.SelectedIndex = 0 sqlText = "SELECT ID, FullName FROM CodeLocation " & _ "ORDER BY FullName" dbInfo = CreateReader(sqlText) Do While dbInfo.Read ProcessLocation.Items.Add(New ListItemData( _ CStr(dbInfo!FullName), CInt(dbInfo!ID))) Loop dbInfo.Close( ) dbInfo = Nothing RefreshProcessLocation( ) Return ErrorHandler: GeneralError("MainForm.TaskProcess", Err.GetException( )) Resume Next
Each time the user selects a different location from the list, we need to update the status display. Add the following code to the ProcessLocation_SelectedIndexChanged
event handler.
Insert Chapter 22, Snippet Item 16.
' ----- Update the status based on the current location. RefreshProcessLocation( )
Daily processing occurs when the user clicks on the Process button. Add the following code to the ActDoProcess_Click
event handler.
Insert Chapter 22, Snippet Item 17.
' ----- Process all of the checked-out books. Dim sqlText As String Dim dbTable As Data.DataTable Dim dbRow As Data.DataRow Dim locationID As Integer On Error GoTo ErrorHandler Me.Cursor = Cursors.WaitCursor ' ----- Get the list of all items that likely need processing. sqlText = "SELECT PC.ID FROM PatronCopy AS PC " & _ "INNER JOIN ItemCopy AS IC ON PC.ItemCopy = IC.ID "& _ "WHERE PC.Returned = 0 AND PC.Missing = 0 " & _ "AND IC.Missing = 0 AND PC.DueDate < " & DBDate(Today) & _ " AND (PC.ProcessDate IS NULL OR PC.ProcessDate < " & _ DBDate(Today) & ")" If (ProcessLocation.SelectedIndex <> −1) Then locationID = CInt(CType(ProcessLocation.SelectedItem, _ ListItemData)) If (locationID <> −1) Then sqlText &= _ " AND IC.Location = " & locationID Else locationID = −1 End If dbTable = CreateDataTable(sqlText) For Each dbRow In dbTable.Rows DailyProcessByPatronCopy(CInt(dbRow!ID), Today) Next dbRow dbTable.Dispose( ) dbTable = Nothing Me.Cursor = Cursors.Default MsgBox("Processing complete.", MsgBoxStyle.OkOnly Or _ MsgBoxStyle.Information, ProgramTitle) ' ----- Update the processing date. sqlText = "UPDATE CodeLocation SET LastProcessing = " & _ DBDate(Today) If (locationID <> −1) Then sqlText &= _ " WHERE ID = " & locationID ExecuteSQL(sqlText) ' ----- Update the status display. ProcessStatus.Text = " Processing is up to date." ProcessStatus.ImageIndex = StatusImageGood Return ErrorHandler: GeneralError("MainForm.ActDoProcess_Click", Err.GetException( )) Resume Next
To try out the code, run it, locate a valid license file, and test out the different administrative features.
This marks the end of primary coding for the Library Project. Congratulations! But there’s still plenty to do, as you can tell by the presence of four more chapters. Now would not be the time to close the book and call it a day. But it would be a good time to learn about ASP.NET, the topic of the next chapter.
3.139.97.53