Introduction
This series of articles discusses three typical requirements when working with file resources (by that I mean, files that have been compiled into your program as resources):
Part 1 | Text and binary files |
Part 2 | Zip files |
Part 3 | Encrypted files |
Embedding an Encrypted File as a Resource
In Part 1, I covered some general topics about resources, and presented CResourceFile
and CResourceTextFile
, classes that allow you to read binary and text files that have been embedded as resources in your app.
In Part 2, I talked about embedding zip files as resources, and presented CResourceZip
, a class that allows you to open and extract entries from a zip file that has been embedded as a resource in your app.
In this article, I will talk about CResourceDecrypt
, a class that allows you to decrypt resources. As with the other classes in this series, the decryption works on memory buffers, so that nothing gets written to disk.
Creating the Encrypted File
I wanted to use the same zip file that I presented in Part 2, but I needed to encrypt it first. So I created a small console program encrypt to accept some command line arguments:
Choosing an Encryption Algorithm
Now I came to the decision that I had been putting off. I wanted an encryption algorithm stronger than XOR, but I did not want a large footprint, or very slow decryption times. I tested many, but most were either too complicated to set up, or too slow, or I simply did not know if they could be trusted. It is surprising to me that there is no single Internet site that is considered the trusted authority on encryption algorithms, that would allow you to compare performance, size, and relative security, and would be able to give you some idea of what algorithm is good for what purpose.
I finally chose TEA, or Tiny Encryption Algorithm, that has been around for over ten years, and is considered to be one of the fastest, and offers moderate security. How complicated is it? Here is the entire code for TEA decryption:
void TeaDecipher(const unsigned long *key,
const unsigned long *inbuf,
unsigned long *outbuf)
{
const unsigned long delta = 0x9E3779B9;
register unsigned long sum = 0xC6EF3720;
register unsigned long y = inbuf[0], z = inbuf[1];
register unsigned long n = 32;
while (n-- > 0)
{
z -= (y << 4 ^ y >> 5) + y ^ sum + key[sum >> 11 & 3];
sum -= delta;
y -= (z << 4 ^ z >> 5) + z ^ sum + key[sum & 3];
}
outbuf[0] = y;
outbuf[1] = z;
}
Key Size and Format
After working with it for a while, the only downside I could find to using TEA was the key. TEA requires keys with a length of exactly 16 bytes. I kept trying to rationalize this, but I did not like it much, and I started to wonder if TEA was the right choice.
Then one day I was browsing a Usenet newsgroup, and came across a post that was talking about verifying data values by using 128-bit MD5 hash algorithm. I kept browsing, but a few minutes later, I thought, Hey! 128 bits is 16 bytes! So then I knew I could use keys consisting of plain text, 1 or 100 characters, and MD5 would give me the 16 bytes I needed for TEA. In the encrypt program I show above, and in the demo app, you will see MD5 called to generate the key for CResourceDecrypt
.
Thanks to George Anescu for his article The FileDigest program and the C++ implementation of some Message-Digest algorithms for the MD5 code.
Working with Zip Files
For this article's demo program, I have set up two resources - one for unencrypted zip file (same as previous article), and one for encrypted zip file. Here are the actual lines taken from XResFileTest.rc:
IDU_ZIP BINARY DISCARDABLE "test_files.zip"
IDU_ENC BINARY DISCARDABLE "test_files.enc"
For a detailed description of this Resource Compiler directive, please see Part 1.
The contents of the zip file is the same as shown in Part 2, so I will get right to the demo app:
The demo app has the same three options as for the last article, but also has a new section to select the encrypted resource:
- You can selected the encrypted resource by checking the checkbox. You can also change the decryption key (and reset it!). The default key is what was used to encrypt the file.
- When the encrypted resource is selected, you will see these messages when you pick an option to perform.
The following code from the demo app shows how encrypted resources are handled:
CResourceZip rz;
m_List.Printf(CXListBox::Blue, CXListBox::White, 0, _T(""));
m_List.Printf(CXListBox::Blue, CXListBox::White, 0,
_T("=== Displaying zip entry %d ==="), m_nViewIndex);
BYTE *decryptbuf = NULL;
if (m_bEnc)
{
int nLen = GetDecryptedBuffer(&decryptbuf);
ASSERT(decryptbuf);
if (decryptbuf)
{
m_List.Printf(CXListBox::Green, CXListBox::White, 0,
_T("\tEncrypted resource opened OK"));
rz.SetByteBuffer(decryptbuf, nLen);
rc = TRUE;
}
else
{
m_List.Printf(CXListBox::Red, CXListBox::White, 0,
_T("\tFailed to decrypt %d"), m_nViewIndex);
}
}
else
{
m_List.Printf(CXListBox::Black, CXListBox::White, 0,
_T("\tOpening unencrypted resource IDU_ZIP"));
rc = rz.Open(NULL, _T("IDU_ZIP"));
}
if (rc)
{
m_List.Printf(CXListBox::Green, CXListBox::White, 0,
_T("\tZip resource opened OK"));
int nCount = rz.GetCount();
if (nCount == ZIP_ENTRIES)
{
Here is the GetDecryptedBuffer()
function:
int CXResFileTestDlg::GetDecryptedBuffer(BYTE ** ppBuf)
{
int nLen = 0;
BYTE *decryptbuf = NULL;
char szKey[200] = { 0 };
#ifdef _UNICODE
WideCharToMultiByte(CP_ACP, 0, m_strKey, -1,
(LPSTR)szKey, sizeof(szKey)-2, NULL, NULL);
#else
_tcsncpy(szKey, m_strKey, sizeof(szKey)-2);
#endif
CMD5 md5;
md5.AddData((BYTE *) szKey, strlen(szKey));
BYTE * pKey = md5.FinalDigest();
ASSERT(pKey);
m_List.Printf(CXListBox::Black, CXListBox::White, 0,
_T("\tOpening encrypted resource IDU_ENC"));
CResourceDecrypt rd;
BOOL rc = rd.Open(NULL, _T("IDU_ENC"), _T("BINARY"), pKey); 3
if (rc)
{
decryptbuf = rd.DetachByteBuffer();
ASSERT(decryptbuf);
if (ppBuf)
{
*ppBuf = decryptbuf;
nLen = rd.GetLength();
}
}
return nLen;
}
Here is what this code is doing:
- The key string (as input by user) must be converted to ANSI, since that is what was used to encrypt the file (with encrypt command-line utility - see above).
- The MD5 hash algorithm is used to generate 16-byte key from input key string. No matter how long the input key string, MD5 generates 16 bytes.
- The final step is to use
CResourceDecrypt
to open the encrypted resource and return a pointer to the decrypted buffer.
Summary: Reading Encrypted Files
The code presented above can be boiled down to:
CResourceDecrypt rd;
BOOL rc = rd.Open(NULL, _T("IDU_ENC"), _T("BINARY"), pKey);
if (rc)
{
int nLen = rd.GetLength();
BYTE *decryptbuf = rd.DetachByteBuffer();
if (decryptbuf)
{
CResourceZip rz;
rz.SetByteBuffer(decryptbuf, nLen);
int nCount = rz.GetCount();
if (nCount != 0)
{
--- do something ---
}
delete [] decryptbuf;
}
}
CResourceDecrypt Quick Reference
Here is a complete list of functions available in CResourceDecrypt
:
How to Add CResourceDecrypt to Your Project
To integrate CResourceDecrypt
class into your app, do the following:
- You first need to add the following files to your project:
- ResourceFile.cpp
- ResourceFile.h
- ResourceDecrypt.cpp
- ResourceDecrypt.h
- tea.cpp
- tea.h
Depending on the type of resource, you may also wish to include the following:
- ResourceZip.cpp
- ResourceZip.h
- XString.cpp
- XString.h
- XUnzip.cpp
- XUnzip.h
- ResourceTextFile.cpp
- ResourceTextFile.h
And depending on how you want to set up decryption, you may also want to include:
- In Visual Studio settings, select Not using pre-compiled header for all the above .cpp files that you have included.
- Next, include the header file ResourceDecrypt.h in the source file where you want to use
CResourceDecrypt
. - Now you are ready to start using
CResourceDecrypt
. See above for sample code.
Implementation Notes
CResourceDecrypt
has been implemented using C++, without any MFC or STL. It has been compiled under both VS6 and VS2005, and has been tested on XP and Vista. It should also work on Win2000 and Win98, but has not been tested on those platforms.
Summary
I have presented a class that allows you to decrypt an encrypted resource file, and access contents of that resource via memory buffer. Using CResourceZip
class I presented in Part 2, and CResourceTextFile
class I presented in Part 1, you can read the text file entries - stored in an encrypted zip file - line by line, just like you can with disk files.
In Part 2 I said that "Embedding zip files as resources provides a significant improvement in terms of protecting your resources from being ripped." Now you can improve on that even further, by encrypting the zip file. Without significant expertise in cryptography, it is impossible to hack a zip file that has been encrypted with TEA.
If you do not wish to use TEA, you also have the opportunity to use another decryption algorithm, by overriding the virtual CResourceDecrypt::Decrypt()
function.
The weakest link in this scheme, of course, is the key. The first thing that a hacker would do is to use the strings utility on your EXE, that would immediately reveal all strings. For example, strings in XResFileTestPt3.exe are seen here:
There are many more strings, of course - this just shows strings longer than 10 characters. The interesting thing to note is names of all the functions. If you must use plain-text string as the key, disguising it as a function name would be one option. A second option would be to disguise plain-text string with XOR or other simple algorithm. A third option - if it is possible - is to not store the key in EXE at all, but have the user enter it when your app starts up.
The approach that I have used in this article is to use an MD5 hash, that would not show up as plain-text key. Of course, given time, a hacker could trace through code execution, and find where the key is stored. For that reason, you may want to break up MD5 hash key into several pieces, and assemble them on the fly. This would make it more difficult - although not impossible - to hack.
Finally, don't forget to use integer resource IDs - these make your app much less transparent than do string resource IDs. (See Part 1 for more details.)
Revision History
Version 1.0 — 2007 July 9
Usage
This software is released into the public domain. You are free to use it in any way you like, except that you may not sell this source code. If you modify it or extend it, please consider posting new code here for everyone to share. This software is provided "as is" with no expressed or implied warranty. I accept no liability for any damage or loss of business that this software may cause.