Downloads
Introduction
A popular method of product validation is using keys similar to VJJJBX-H2BBCC-68CF7F-2BXD4R-3XP7FB-JDVQBC. These compact keys can be derived using Symmetric Key Cryptosystems such as the Advanced Encryption Standard (AES).
Other Public Key Cryptosystems are available such as RSA. However, these systems generally produce larger keys (which the user will eventually have to enter into the program to unlock functionality). Smaller producing Cryptosystems exist, but it is the author's opinion that they are highly encumbered with patents. Quartz is one such example. It is a Public Key Encryption System that produces a smaller cipher text based on Hidden Field Equations (HFEs). The Quartz website is littered with phrases such as "must license" and "pay royalties".
The reader is also encouraged to investigate Signature Schemes (with Recovery) as an alternative method to producing Product Keys. An example is PSS-R, a Message Recovery Signature Scheme based on RSA. PSS-R is not suitable for product keys due to the size of the resulting key. However, cryptosystems such as a Weil Pairing system should be of interest. Once Weil Pairing is finalized in committee, it will be added to the Crypto++ library.
Finally, the reader should also visit Product Keys Based on Elliptic Curve Cryptography to familiarize themselves with basic concepts of Product Keys in the domain of Public Key Cryptography; and Product Activation Based on RSA Signatures.
This article will use AES (specified in FIPS 197) as the Cryptosystem, and Wei Dai's Crypto++ for AES operations. AES will produce compact keys with the additional benefit that the cryptosystem is not burdened with patent compliance. However, should a binary fall to Reverse Engineering, the key will become compromised (note that AES is a Symmetric Cipher - not an Asymmetric Cipher which has Public and Private keys).
This article will discuss the following topics:
- Advanced Encryption Standard
- Compiling and Integrating Crypto++ into the Visual C++ Environment
- AES Implementation in Crypto++
- Base Encoding a Cipher Text String in Crypto++
- Bulk Product Key Generation
- Product Key Validation
- Securely Saving Key or Activation State to the Registry
This article is based on the Visual C++ 6.0 Environment in hopes that it reaches the largest audience.
Advanced Encryption Standard
Currently, there are three FIPS approved symmetric encryption algorithms: AES, Triple DES, and Skipjack. This article will use AES or the Advanced Encryption Standard in CBC Mode. Note that DES (FIPS 46-3) was withdrawn in May 2005, and is no longer approved for Federal use.
AES (or Rijndeal - pronounced "Rhine dahl") is the work of Joan Daemen and Vincent Rijmen - hence the portmanteau Rijndael. AES is a 128 bit block cipher that accepts key lengths of 128, 192, and 256 bits. The required number of rounds (i.e., linear and non-linear transformations), depend on the key size. Below are the FIPS 197 conformant Key-Block-Round combinations.
Taking from FIPS 197:
For both its Cipher and Inverse Cipher, the AES algorithm uses a round function that is composed of four different byte-oriented transformations: 1) byte substitution using a substitution table (S-box), 2) shifting rows of the State array by different offsets, 3) mixing the data within each column of the State array, and 4) adding a Round Key to the State. These transformations (and their inverses) are described in Sections 5.1.1-5.1.4 and 5.3.1-5.3.4.
Compiling and Integrating Crypto++ into the Microsoft Visual C++ Environment
Please see the related article, Compiling and Integrating Crypto++ into the Microsoft Visual C++ Environment. This article is based upon basic assumptions presented in the previously mentioned article.
For those who are interested in other C++ Number Theoretic libraries, please see Peter Gutmann's Cryptlib or Victor Shoup's NTL.
AES Implementation in Crypto++
The first step in developing the system is to demonstrate AES in Crypto++. The following samples will present an alternate (and more elegant) method than which was presented in An AES Encrypting Registry Class.
Above is the result of running aestest1. Below is the source code. Following the source code is a brief explanation with respect to the Crypto++ Library.
#include <iostream>
#include <iomanip>
#include "cryptlib.h"
#include "aes.h" // AES
#include "modes.h" // CBC_Mode< >
#include "filters.h" // StringSource
int main(int argc, char* argv[]) {
byte key[ CryptoPP::AES::DEFAULT_KEYLENGTH ],
iv[ CryptoPP::AES::BLOCKSIZE ];
::memset( key, 0x01, CryptoPP::AES::DEFAULT_KEYLENGTH );
::memset( iv, 0x01, CryptoPP::AES::BLOCKSIZE );
std::string PlainText = "Hello AES World";
std::cout << "Plain Text:" << std::endl;
std::cout << " '" << PlainText << "'" << std::endl;
std::cout << std::endl;
std::string CipherText;
CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption
Encryptor( key, sizeof(key), iv );
CryptoPP::StringSource( PlainText, true,
new CryptoPP::StreamTransformationFilter( Encryptor,
new CryptoPP::StringSink( CipherText )
) );
std::cout << "Cipher Text (" << CipherText.size() <<") bytes:"
<< std::endl;
for(unsigned int i = 0; i < CipherText.size(); i++ )
{
if( 0 != i && 10 == i ) { std::cout << std::endl; }
std::cout << std::hex << "0x";
std::cout << ( static_cast<unsigned>( 0xFF & CipherText[ i ] ) )<<" ";
}
std::cout << std::endl << std::endl;
std::string RecoveredText;
CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption
Decryptor( key, sizeof(key), iv );
CryptoPP::StringSource( CipherText, true,
new CryptoPP::StreamTransformationFilter( Decryptor,
new CryptoPP::StringSink( RecoveredText )
) );
std::cout << "Recovered Text:" << std::endl;
std::cout << " '" << RecoveredText << "'" << std::endl;
std::cout << std::endl;
return 0;
}
Example 1 is quite busy. First, the program sets up a Key and IV. All Crypto++ Symmetric Ciphers define a value for DEFAULT_KEYLENGTH
and BLOCKSIZE
. The Key and IV is then Initialized.
::memset( key, 0x01, CryptoPP::BLOCK_CIPHER::DEFAULT_KEYLENGTH );
::memset( iv, 0x01, CryptoPP::BLOCK_CIPHER::BLOCKSIZE );
In later examples, the Key and IV will be initialized to a pseudo random value. The same Key, IV, and Mode must be used to decrypt the cipher text that was used to encrypt the plain text.
The next noteworthy piece of code follows. After executing, the encryption object is now ready for use.
CryptoPP::CTR_Mode<CryptoPP::AES>::Encryption Encryptor(key, sizeof(key), iv);
The following will discus the Filter Chaining paradigm in Crypto++. For a more in depth discussion of Filter Chaining, see the Crypto++ Wiki Pages.
CryptoPP::StringSource( PlainText, true, new
CryptoPP::StreamTransformationFilter( Encryptor,
new CryptoPP::StringSink( CipherText )
)
);
A StingSource
is created. The string source will take the string and push it into the BufferedTransformation
(StreamTransformationFilter
). It should be logically observed as depicted below.
Crypto++ provides multiple Sources to use:
- FileSource
- SocketSource
- NetworkSource
- WindowsPipeSource
Just as the Source is the origin of the data, the Sink is the destination endpoint of the data. Note that even though a std::string
is used, one should not view the string classically. A better choice of abstractions is a rope. Below is the logical diagrams of a StringSink
.
Crypto++ provides multiple Sinks to use as destination endpoints:
- FileSink
- SocketSink
- NetworkSink
- WindowsPipeSink
The remaining item to discuss it the transformation, depicted below.
BufferedTransformation
is used because it is the base class of all transforms. Data is received from a previous transform (or a Source), the data is operated upon, and the data is passed out (to another transform or a Sink). The final view of the process is shown below. A StreamTransformationFilter
is a more complex filter. Other filters exists, such as HexEncoders or Base32Encoders which are more intuitive.
The "Identity" transformation would be viewed as follows (though this is a valid Crypto++ construct, it is not very useful).
CryptoPP::StringSource( source, true,
new CryptoPP::StringSink( sink )
);
It is very noteworthy that the nameless objects created with new
will be deleted by the hosting object when they are no longer needed (this is not the case when an object receives a Reference). So, in this example:
- The
StreamTransformationFilter
will delete the StringSink
object when no longer required - The
StringSource
will delete the StreamTransformationFilter
object when no longer required - The cleanup process begins when the
StringSource
destructor is invoked
The second sample provides a generalization for using Symmetric Ciphers in Crypto++ to the reader for convenience. By #define
an appropriate cipher and mode, the reader may fully test the capabilities of Crypto++. aestest2 issues #include "BlockCiphers.h"
which simply brings in the various Crypto++ header files required for a cipher selection. The following three examples display output from aestest2.
|
Triple DES in CFB Mode |
|
RC6 in CTR Mode |
|
Rijndael CBC Mode |
The various Ciphers and Modes inherit from common base classes (BlockCipherDocumentation or CipherModeDocumentation). The partial Inheritance Diagrams are below.
|
Cipher Mode Documentation Partial Inheritance Diagram |
The use of the various XXX_Mode_ExternalCipher
was presented in An AES Encrypting Registry Class. In the author's opinion, their use is less fashionable.
|
Block Documentation Partial Inheritance Diagram |
Base Encoding a Cipher Text String in Crypto++
Keeping in spirit of the Filter chaining paradigm, aestest3 uses a Base32 Encoder to produce a human readable cipher text string. Base32 Encoding expands the cipher text to 26 characters. The number 26 is derived as follow:
- 15 or fewer plain text characters are encrypted to 16 cipher text characters (1 Block)
- 16 (cipher text characters) * 1.6 (60% Base32 encoding expansion) = 25.6
26 characters do not group well. This will be addressed later in the article. BaseExp is available for download which demonstrates the base encoding expansion should the reader desire.
The note worthy addition to aestest3 is the following. Notice the addition of the Base32 Filter.
CryptoPP::StringSource( PlainText, true,
new CryptoPP::StreamTransformationFilter( Encryptor,
new CryptoPP::Base32Encoder(
new CryptoPP::StringSink( EncodedText )
) ) );
Decryption unwinds the process, in reverse order.
CryptoPP::StringSource( EncodedText, true,
new CryptoPP::Base32Decoder(
new CryptoPP::StreamTransformationFilter( Decryptor,
new CryptoPP::StringSink( RecoveredText )
) ) );
#include <iostream>
#include "cryptlib.h"
#include "Base32.h"
#include "aes.h" // AES
#include "modes.h" // CBC_Mode< >
#include "filters.h" // StringSource and
int main(int argc, char* argv[]) {
byte key[ CryptoPP::AES::DEFAULT_KEYLENGTH ],
iv[ CryptoPP::AES::BLOCKSIZE ];
::memset( key, 0x01, CryptoPP::AES::DEFAULT_KEYLENGTH );
::memset( iv, 0x01, CryptoPP::AES::BLOCKSIZE );
CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption
Encryptor( key, sizeof(key), iv );
std::string PlainText = "Hello World";
std::string EncodedText;
CryptoPP::StringSource( PlainText, true,
new CryptoPP::StreamTransformationFilter( Encryptor,
new CryptoPP::Base32Encoder(
new CryptoPP::StringSink( EncodedText )
) ) );
std::string RecoveredText;
CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption
Decryptor( key, sizeof(key), iv );
CryptoPP::StringSource( EncodedText, true,
new CryptoPP::Base32Decoder(
new CryptoPP::StreamTransformationFilter( Decryptor,
new CryptoPP::StringSink( RecoveredText )
) ) );
...
return 0;
}
Bulk Product Key Generation
This portion of the article will address the formatting needs of the Product Key, and serve as proof of concept for the Key Generator. KeyAndIVGen is used to generate the AES Key and AES IV. Note this is different from the Product Key. This step is only required once per set of Product Keys.
In aestest4, each iteration of the loop changes the Encryption and Decryption object, so each object's state will have to be reset at each iteration:
std::string EncodedText = "";
std::string SaltText = "";
Encryptor.Resynchronize( iv );
Decryptor.Resynchronize( iv );
By invoking an alternate Base32Encoder constructor, one can form the Product Key using Crypto++. Note that there is no Base32Decoder equivalent. If the character encountered is not over the alphabet, it is silently consumed. Crypto++ currently implements the Differential Unicode Domain Encoding (DUDE) as specified in the IETF draft. Should DUDE not suffice the reader, he or she should research MACE: Modal ASCII Compatible Encoding for IDN; or implement their own Base32 Encoder.
CryptoPP::StringSource( PlainText, true,
new CryptoPP::StreamTransformationFilter( Encryptor,
new CryptoPP::Base32Encoder(
new CryptoPP::StringSink( EncodedText ),
true, 4, "-") ) );
Unlike ECIES (which creates a Temporary Public Key V for each cipher text object), AES will require salt to randomize the keys.
4 bytes of random salt is added below. This changes the encryption operation as follows.
CryptoPP::RandomNumberSource( rng, 4, true,
new CryptoPP::StringSink( SaltText )
);
...
CryptoPP::StringSource( SaltText + PlainText, true,
new CryptoPP::StreamTransformationFilter( Encryptor,
new CryptoPP::Base32Encoder(
new CryptoPP::StringSink( EncodedText ),
true, 4, "-") ) );
The final step in Proof of Concept is stepping over the salt, adding a 2 character appendage after Base32 encoding, and removing the appendage before Base32 decoding. Each are trivially implemented in aestest5.
#include <iostream>
#include "cryptlib.h"
#include "osrng.h" // PRNG
#include "Base32.h" // Base32
#include "aes.h" // AES
#include "modes.h" // CBC_Mode< >
#include "filters.h" // StringSource and
int main(int argc, char* argv[]) {
byte key[ CryptoPP::AES::DEFAULT_KEYLENGTH ],
iv[ CryptoPP::AES::BLOCKSIZE ];
::memset( key, 0x01, CryptoPP::AES::DEFAULT_KEYLENGTH );
::memset( iv, 0x01, CryptoPP::AES::BLOCKSIZE );
const std::string PlainText = "Hello AES";
CryptoPP::AutoSeededRandomPool rng;
CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption
Encryptor( key, sizeof(key), iv );
CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption
Decryptor( key, sizeof(key), iv );
std::cout << "Algorithm:" << std::endl;
std::cout << " " << Encryptor.AlgorithmName() << std::endl;
std::cout << std::endl;
std::cout << "Plain Text (" << PlainText.length() << " bytes)"<<std::endl;
std::cout << " '" << PlainText << "'" << std::endl;
std::cout << std::endl;
unsigned int ITERATIONS = 4;
for( unsigned int i = 0; i < ITERATIONS; i++ )
{
std::string EncodedText = "";
std::string SaltText = "";
Encryptor.Resynchronize( iv );
Decryptor.Resynchronize( iv );
CryptoPP::RandomNumberSource( rng, 4, true,
new CryptoPP::StringSink( SaltText )
);
CryptoPP::StringSource( SaltText + PlainText, true,
new CryptoPP::StreamTransformationFilter( Encryptor,
new CryptoPP::Base32Encoder(
new CryptoPP::StringSink( EncodedText ),
true, 4, "-") ) );
EncodedText += "JW";
std::cout << EncodedText << std::endl;
std::string RecoveredText = "";
EncodedText = EncodedText.substr( 0, EncodedText.length() - 2 );
CryptoPP::StringSource( EncodedText, true,
new CryptoPP::Base32Decoder(
new CryptoPP::StreamTransformationFilter( Decryptor,
new CryptoPP::StringSink( RecoveredText )
) ) );
RecoveredText = RecoveredText.substr( 4 );
std::cout << " '" << RecoveredText << "'" << std::endl;
}
return 0;
}
aestest5 is the final step before producing the key generator. This sample builds upon aestest4 in ways described in the following.
First, the program defines some useful features that will be made available to the user. Note that the values span byte boundaries for demonstration purposes.
const unsigned int FEATURE_EVALUATION = 0x01; const unsigned int FEATURE_USE_SQUARES = 0x02; ...
const unsigned int FEATURE_USE_POLYGONS = 0x0400; const unsigned int FEATURE_USE_PENTAGONS = 0x0800;
A Prologue is printed with Algorithm Name, Key, and IV. This was added to demonstrate the ability to attach to a filter.
CryptoPP::HexEncoder KeyEncoder( NULL, true, 2 );
KeyEncoder.Attach( new CryptoPP::StringSink( HexKey ) );
KeyEncoder.PutMessageEnd( key, keylen );
The program then performs as aestest4 until just after entering the generating loop. The reader should notice that Features (though defined as a bit mapped value) are encoded into a string. The string is randomly populated. This is simply a matter of expediency.
std::string FeatureText = "";
std::string SaltText = "";
...
Encryptor.Resynchronize( iv );
Decryptor.Resynchronize( iv );
CryptoPP::RandomNumberSource( rng, 4, true,
new CryptoPP::StringSink( FeatureText )
);
The next diversion appears during encryption.
CryptoPP::StringSource( SaltText + MagicText + FeatureText, true,
new CryptoPP::StreamTransformationFilter( Encryptor,
new CryptoPP::Base32Encoder(
new CryptoPP::StringSink( EncodedText ),
true, 4, "-") ) );
Decryption unwinds the encryption process. During encryption, the plain text was a concatenation of three strings: Salt, Magic, and Data. Extraction is slightly different - the program has one std::string
to parse which appears as follows in memory (note that the Unused Bytes are present to remind the reader to not exceed 15 Bytes):
RecoveredMagic = *( (unsigned int*)(RecoveredText.substr( 0, 4 ).data() ) );
RecoveredText = RecoveredText.substr( 4 );
Notes on the obfuscation:
- RecoveredText.substr( 0, 4 ) returns a string comprised of the first 4 bytes
- RecoveredText.substr( 0, 4 ).data() returns a byte pointer to the data
(unsigned int*)
assists the compiler in generating the desired code. Otherwise, the dereference would only extract byte[ 0 ]
, not byte[ 0 - 3 ]
as desired.
The point is stressed because should the reader improperly extract Magic
, the program will assert in Debug mode, or reject a valid Product Key in Release builds.
assert( FEATURE_MAGIC == RecoveredMagic );
|
Proper Cast of data() Buffer |
|
Improper Cast of data() Buffer |
#include <iostream>
#include "cryptlib.h"
#include "osrng.h" // PRNG
#include "Base32.h" // Base32
#include "hex.h" // Hex
#include "aes.h" // AES
#include "modes.h" // CBC_Mode< >
#include "filters.h" // StringSource and
const unsigned int FEATURE_EVALUATION = 0x01; const unsigned int FEATURE_USE_SQUARES = 0x02; const unsigned int FEATURE_USE_CIRCLES = 0x04; const unsigned int FEATURE_USE_WIDGETS = 0x08; const unsigned int FEATURE_USE_ELLIPSES = 0x0100; const unsigned int FEATURE_USE_TRIANGLES = 0x0200; const unsigned int FEATURE_USE_POLYGONS = 0x0400; const unsigned int FEATURE_USE_PENTAGONS = 0x0800;
const unsigned int FEATURE_MAGIC = 0xAAAAAAAA;
void PrintPrologue( std::string algorithm, byte* key, int keylen, byte* iv,
int ivlen );
void PrintFeatures( unsigned int features );
int main(int argc, char* argv[]) {
byte key[ CryptoPP::AES::DEFAULT_KEYLENGTH ] =
{ 0x93, 0x33, 0x6B, 0x82, 0xD6, 0x64, 0xB2, 0x46,
0x95, 0xAB, 0x89, 0x91, 0xD3, 0xE5, 0xDC, 0xB0 };
byte iv[ CryptoPP::AES::BLOCKSIZE ] =
{ 0x61, 0x4D, 0xCA, 0x6F, 0xB2, 0x56, 0xF1, 0xDB,
0x0B, 0x24, 0x5D, 0xCF, 0xB4, 0xBD, 0xB6, 0xD3 };
CryptoPP::AutoSeededRandomPool rng;
CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption
Encryptor( key, sizeof(key), iv );
CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption
Decryptor( key, sizeof(key), iv );
const std::string MagicText( 4, (char)0xAA );
PrintPrologue( Encryptor.AlgorithmName(),
key, sizeof(key), iv, sizeof(iv) );
unsigned int ITERATIONS = 4;
for( unsigned int i = 0; i < ITERATIONS; i++ )
{
std::string FeatureText = "";
std::string SaltText = "";
std::string EncodedText = "";
Encryptor.Resynchronize( iv );
Decryptor.Resynchronize( iv );
CryptoPP::RandomNumberSource( rng, 4, true,
new CryptoPP::StringSink( SaltText )
);
CryptoPP::RandomNumberSource( rng, 4, true,
new CryptoPP::StringSink( FeatureText )
);
CryptoPP::StringSource( SaltText + MagicText + FeatureText, true,
new CryptoPP::StreamTransformationFilter( Encryptor,
new CryptoPP::Base32Encoder(
new CryptoPP::StringSink( EncodedText ),
true, 4, "-") ) );
EncodedText += "JW";
std::cout << EncodedText << std::endl;
std::string RecoveredText = "";
EncodedText = EncodedText.substr( 0, EncodedText.length() - 2 );
CryptoPP::StringSource( EncodedText, true,
new CryptoPP::Base32Decoder(
new CryptoPP::StreamTransformationFilter( Decryptor,
new CryptoPP::StringSink( RecoveredText )
) ) );
unsigned int RecoveredSalt =
*( (unsigned int*)(RecoveredText.substr( 0, 4 ).data() ) );
RecoveredText = RecoveredText.substr( 4 );
unsigned int RecoveredMagic =
*( (unsigned int*)(RecoveredText.substr( 0, 4 ).data() ) );
RecoveredText = RecoveredText.substr( 4 );
assert( FEATURE_MAGIC == RecoveredMagic );
unsigned int RecoveredFeatures =
*( (unsigned int*)(RecoveredText.substr( 0, 4 ).data() ) );
RecoveredText = RecoveredText.substr( 4 );
PrintFeatures( RecoveredFeatures );
}
return 0;
}
void PrintPrologue( std::string algorithm, byte* key,
int keylen, byte* iv, int ivlen )
{
std::string HexKey, HexIV;
CryptoPP::HexEncoder KeyEncoder( NULL, true, 2 );
KeyEncoder.Attach( new CryptoPP::StringSink( HexKey ) );
KeyEncoder.PutMessageEnd( key, keylen );
CryptoPP::HexEncoder IVEncoder( NULL, true, 2 );
IVEncoder.Attach( new CryptoPP::StringSink( HexIV ) );
IVEncoder.PutMessageEnd( iv, ivlen );
std::cout << algorithm << std::endl;
std::cout << "key[] = " << HexKey << std::endl;
std::cout << " iv[] = " << HexIV << std::endl;
std::cout << std::endl;
}
void PrintFeatures( unsigned int features )
{
if( FEATURE_EVALUATION == ( features & FEATURE_EVALUATION ) )
{ std::cout << "Evaluation Edition" << std::endl; }
if( FEATURE_USE_SQUARES == ( features & FEATURE_USE_SQUARES ) )
{ std::cout << "Operations are permitted on Squares" << std::endl; }
if( FEATURE_USE_CIRCLES == ( features & FEATURE_USE_CIRCLES ) )
{ std::cout << "Operations are permitted on Circles" << std::endl; }
if( FEATURE_USE_WIDGETS == ( features & FEATURE_USE_WIDGETS ) )
{ std::cout << "Operations are permitted on Widgets" << std::endl; }
if( FEATURE_USE_ELLIPSES == ( features & FEATURE_USE_ELLIPSES ) )
{ std::cout << "Operations are permitted on Ellipses" << std::endl; }
if( FEATURE_USE_POLYGONS == ( features & FEATURE_USE_POLYGONS ) )
{ std::cout << "Operations are permitted on Polygons" << std::endl; }
if( FEATURE_USE_TRIANGLES == ( features & FEATURE_USE_TRIANGLES ) )
{ std::cout << "Operations are permitted on Triangles" << std::endl; }
if( FEATURE_USE_PENTAGONS == ( features & FEATURE_USE_PENTAGONS ) )
{ std::cout << "Operations are permitted on Pentagons" << std::endl; }
std::cout << std::endl;
}
KeyGen is based on aestest5. Feature encoding is no longer implemented as a random function:
unsigned int Features = 0;
Features |= FEATURE_USE_ELLIPSES;
Features |= FEATURE_USE_PENTAGONS;
EncodeFeatures( FeatureText, Features );
...
void EncodeFeatures( std::string& FeatureText, unsigned int features )
{
assert( 4 == sizeof( unsigned int ) );
char c = '\0';
c = ( features << 0 ) & 0xFF;
FeatureText += c;
c = ( features << 8 ) & 0xFF;
FeatureText += c;
c = ( features << 16 ) & 0xFF;
FeatureText += c;
c = ( features << 24 ) & 0xFF;
FeatureText += c;
}
#include <iostream>
#include "cryptlib.h"
#include "osrng.h" // PRNG
#include "Base32.h" // Base32
#include "hex.h" // Hex
#include "aes.h" // AES
#include "modes.h" // CBC_Mode< >
#include "filters.h" // StringSource and
const unsigned int FEATURE_EVALUATION = 0x01; const unsigned int FEATURE_USE_SQUARES = 0x02; const unsigned int FEATURE_USE_CIRCLES = 0x04; const unsigned int FEATURE_USE_WIDGETS = 0x08; const unsigned int FEATURE_USE_ELLIPSES = 0x0100; const unsigned int FEATURE_USE_TRIANGLES = 0x0200; const unsigned int FEATURE_USE_POLYGONS = 0x0400; const unsigned int FEATURE_USE_PENTAGONS = 0x0800;
const unsigned int FEATURE_MAGIC = 0xAAAAAAAA;
void PrintPrologue( std::string algorithm, byte* key, int keylen, byte* iv,
int ivlen );
void EncodeFeatures( std::string& FeatureText, unsigned int features );
void PrintFeatures( unsigned int features );
int main(int argc, char* argv[]) {
byte key[ CryptoPP::AES::DEFAULT_KEYLENGTH ] =
{ 0x93, 0x33, 0x6B, 0x82, 0xD6, 0x64, 0xB2, 0x46,
0x95, 0xAB, 0x89, 0x91, 0xD3, 0xE5, 0xDC, 0xB0 };
byte iv[ CryptoPP::AES::BLOCKSIZE ] =
{ 0x61, 0x4D, 0xCA, 0x6F, 0xB2, 0x56, 0xF1, 0xDB,
0x0B, 0x24, 0x5D, 0xCF, 0xB4, 0xBD, 0xB6, 0xD3 };
CryptoPP::AutoSeededRandomPool rng;
CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption
Encryptor( key, sizeof(key), iv );
CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption
Decryptor( key, sizeof(key), iv );
const std::string MagicText( 4, (char)0xAA );
PrintPrologue( Encryptor.AlgorithmName(), key,
sizeof(key), iv, sizeof(iv) );
unsigned int ITERATIONS = 3;
for( unsigned int i = 0; i < ITERATIONS; i++ )
{
std::string FeatureText = "";
std::string SaltText = "";
std::string EncodedText = "";
Encryptor.Resynchronize( iv );
Decryptor.Resynchronize( iv );
CryptoPP::RandomNumberSource( rng, 4, true,
new CryptoPP::StringSink( SaltText )
);
unsigned int Features = 0;
Features |= FEATURE_USE_ELLIPSES;
Features |= FEATURE_USE_PENTAGONS;
EncodeFeatures( FeatureText, Features );
CryptoPP::StringSource( SaltText + MagicText + FeatureText, true,
new CryptoPP::StreamTransformationFilter( Encryptor,
new CryptoPP::Base32Encoder(
new CryptoPP::StringSink( EncodedText ),
true, 4, "-") ) );
EncodedText += "JW";
std::cout << EncodedText << std::endl;
std::string RecoveredText = "";
EncodedText = EncodedText.substr( 0, EncodedText.length() - 2 );
CryptoPP::StringSource( EncodedText, true,
new CryptoPP::Base32Decoder(
new CryptoPP::StreamTransformationFilter( Decryptor,
new CryptoPP::StringSink( RecoveredText )
) ) );
unsigned int RecoveredSalt =
*( (unsigned int*)(RecoveredText.substr( 0, 4 ).data() ) );
RecoveredText = RecoveredText.substr( 4 );
unsigned int RecoveredMagic =
*( (unsigned int*)(RecoveredText.substr( 0, 4 ).data() ) );
RecoveredText = RecoveredText.substr( 4 );
assert( FEATURE_MAGIC == RecoveredMagic );
unsigned int RecoveredFeatures =
*( (unsigned int*)(RecoveredText.substr( 0, 4 ).data() ) );
RecoveredText = RecoveredText.substr( 4 );
PrintFeatures( RecoveredFeatures );
}
return 0;
}
void EncodeFeatures( std::string& FeatureText, unsigned int features )
{
assert( 4 == sizeof( unsigned int ) );
...
}
void PrintPrologue( std::string algorithm, byte* key,
int keylen, byte* iv, int ivlen )
{
...
}
void PrintFeatures( unsigned int features )
{
...
}
Product Key Validation
KeyVal is KeyGen less the generation routines. However, since this portion of the article is examining KeyVal, the following are noteworthy.
The Base32 Decoder is fairly resilient. Notice that the Product Key from above is mixed case, and missing one hyphen.
#include <iostream>
#include "cryptlib.h"
#include "Base32.h" // Base32
#include "hex.h" // Hex
#include "aes.h" // AES
#include "modes.h" // CBC_Mode< >
#include "filters.h" // StringSource and
const unsigned int FEATURE_EVALUATION = 0x01; const unsigned int FEATURE_USE_SQUARES = 0x02; const unsigned int FEATURE_USE_CIRCLES = 0x04; const unsigned int FEATURE_USE_WIDGETS = 0x08; const unsigned int FEATURE_USE_ELLIPSES = 0x0100; const unsigned int FEATURE_USE_TRIANGLES = 0x0200; const unsigned int FEATURE_USE_POLYGONS = 0x0400; const unsigned int FEATURE_USE_PENTAGONS = 0x0800;
const unsigned int FEATURE_MAGIC = 0xAAAAAAAA;
void PrintPrologue( std::string algorithm, byte* key, int keylen, byte* iv,
int ivlen );
void PrintFeatures( unsigned int features );
int main(int argc, char* argv[]) {
byte key[ CryptoPP::AES::DEFAULT_KEYLENGTH ] =
{ 0x93, 0x33, 0x6B, 0x82, 0xD6, 0x64, 0xB2, 0x46,
0x95, 0xAB, 0x89, 0x91, 0xD3, 0xE5, 0xDC, 0xB0 };
byte iv[ CryptoPP::AES::BLOCKSIZE ] =
{ 0x61, 0x4D, 0xCA, 0x6F, 0xB2, 0x56, 0xF1, 0xDB,
0x0B, 0x24, 0x5D, 0xCF, 0xB4, 0xBD, 0xB6, 0xD3 };
CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption
Decryptor( key, sizeof(key), iv );
const std::string MagicText( 4, (char)0xAA );
std::string RecoveredText = "";
PrintPrologue( Decryptor.AlgorithmName(), key,
sizeof(key), iv, sizeof(iv) );
std::string EncodedText = "X3bA9NSF-8n9q-UWQC-U7FX-AZZF-JAJW";
std::cout << EncodedText << std::endl;
EncodedText = EncodedText.substr( 0, EncodedText.length() - 2 );
CryptoPP::StringSource( EncodedText, true,
new CryptoPP::Base32Decoder(
new CryptoPP::StreamTransformationFilter( Decryptor,
new CryptoPP::StringSink( RecoveredText )
) ) );
unsigned int RecoveredSalt =
*( (unsigned int*)(RecoveredText.substr( 0, 4 ).data() ) );
assert( RecoveredText.length() >= 4 );
RecoveredText = RecoveredText.substr( 4 );
unsigned int RecoveredMagic =
*( (unsigned int*)(RecoveredText.substr( 0, 4 ).data() ) );
assert( RecoveredText.length() >= 4 );
RecoveredText = RecoveredText.substr( 4 );
assert( FEATURE_MAGIC == RecoveredMagic );
unsigned int RecoveredFeatures =
*( (unsigned int*)(RecoveredText.substr( 0, 4 ).data() ) );
assert( RecoveredText.length() >= 4 );
RecoveredText = RecoveredText.substr( 4 );
PrintFeatures( RecoveredFeatures );
return 0;
}
void PrintPrologue( std::string algorithm, byte* key,
int keylen, byte* iv, int ivlen )
{
...
}
void PrintFeatures( unsigned int features )
{
...
}
Should the user enter a Product Key which is too short or too long, Crypto++ will may throw a CryptoPP::Exception
. The reader's code should set up catch blocks as follows.
try {
...
}
catch( CryptoPP::Exception& e ) {
std::cerr << "Error: " << e.what() << std::endl;
}
catch( std::exception& e ) {
std::cerr << "Error: " << e.what() << std::endl;
}
catch (...) {
std::cerr << "Unknown Error" << std::endl;
}
Securely Saving Key or Activation State to the Registry
Product Keys and Program Activation state can be securely saved to the Windows Registry. Please see An AES Encrypting Registry Class for a discussion and implementation.
Summary
This article presented a Product Key Scheme system based on well established Symmetric Key Cryptosystems. The reader is encouraged to explore other systems such as Hidden Field Equations which may prove useful in producing Product Keys.
Acknowledgments
- Wei Dai for Crypto++ and his invaluable help on the Crypto++ mailing list
- Dr. Brooke Stephens who laid my Cryptographic foundations
Revisions
- 01.07.2007 Reworked Introduction, Added Weil Pairing
- 12.13.2006 Uploaded KeyIVGen Source Code
- 12.13.2006 Added Sample Checksums
- 11.29.2006 Updated Article Links
- 11.21.2006 Initial Release
Checksums
aestest1.zip
MD5: AC0013B5BD677B41029707970B47931E
SHA-1: 72FBE8E42BD5AE6DFCC4334FF94EC0E4FDFF4E01
aestest2.zip
MD5: BCF3832DA5656ACECAC1C6125438A0D4
SHA-1: D873496E54F27AADD396B7BB3C7CBAB0F4D267FC
aestest3.zip
MD5: CF876D81225C4BB76A050E6991BC67A4
SHA-1: ECD8CF75AA0E882205E8F3BAFED864545D032FA6
aestest4.zip
MD5: F44208162E5F8CA88920AF3977976724
SHA-1: 7B76E645FFC64AD7D40A5C5F0EC6C44AFFE4DE50
aestest5.zip
MD5: 61F0A9D1B002415E7A79C14842C4551B
SHA-1: 4CECAC22F6C02D6D14EB51C59BB45EEA5AF01BC2
KeyAndIVGenSrc.zip
MD5: FA48775E7491EBE6D0F2DBA60B3908ED
SHA-1: E71F5DD1B2047CEFD1B2791797813AA6546D5371
KeyAndIVGen.zip (Program)
MD5: FECAA685B5A8E2A3E30D4BB37D9461FF
SHA-1: 84567C60903211DB8071936DF0875A368CE22A08