I was in need of an encryption algorithm to the following requirements:
1) Must be capable of encrypting strings to a byte array, and decyrpting
back again to the same string
2) Must have the same algorithm work with strings that may or may not be
unicode
3) Number of bytes back must either be <= number of _TCHARs in *
sizeof(_TCHAR), or the relation between output size and input size can be
calculated simply. Has to take into account the null terminator on the end
of the string.
4) Encryption algorithm must also return the exact number of bytes of the
encrypted data
I was struggling to get the CryptoAPI to work, with CALG_RC4 it didn't
encrypt at all, and with CALG_RC2 it didn't decrypt at all (both claimed
to have succeeded, but with the former, the encrypted string was exactly
the same as the input, and with the latter, the decrypted string was
exactly the same as the encrypted string) - and I thought I'd done
everything right (at the bottom if you reckon you can spot where I
havent...)
So I decided to invent my own algorithm, and I just wanted anybody's
opinion on how secure this could be compared to the Win32 API version.
First, a C# program generates an array of 256 bytes, as such:
using System;
using System.Security.Cryptography;
class Class1
{
[STAThread]
static void Main()
{
byte[] b_raw = new byte[256];
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
rng.GetBytes(b_raw);
Console.Write("const BYTE b[] = {");
for(int i = 0; i < 256; i += 8)
{
Console.WriteLine("0x{0:x}, 0x{1:x}, 0x{2:x}, 0x{3:x}, 0x{4:x}, 0x{5:x},
0x{6:x}, 0x{7:x},",
b_raw, b_raw[i+1], b_raw[i+2], b_raw[i+3], b_raw[i+4], b_raw[i+5],
b_raw[i+6], b_raw[i+7]);
}
}
}
This is run twice, so I have two arrays of 256 bytes, say key1 and key2.
These are then hardcoded into the C++ encryption algorithm.
Then, the C++ encryption algorithm goes as such:
It memcpys the _TCHAR array to a byte array, then loops round each byte of
this array.
For each byte, it gets the value of key1[n] (where n is the byte number),
and calls this 'b_current_indir' (the starting 'indirection level').
Then, it gets the value of key2[n] and calls this 'levels' - the number of
indirection levels.
Then, an inner loop runs 'levels' times - and on each loop the following
happens: the current byte of the data to be encrypted (dictated by the
outer loop) is XORed with key2[b_current_indir], and THEN, b_current_indir
is reassigned to take on the value of key2[b_current_indir].
The whole C++ algorithm is defined as such:
void Decrypt(BYTE* orig, long bytelen, _TCHAR* dataout)
{
BYTE* b_temp = new BYTE[bytelen];
#ifdef _DEBUG
ZeroMemory(b_temp, bytelen);
#endif
for(long bytenum = 0; bytenum < bytelen; bytenum++)
{
BYTE levels = b[bytenum % 256],
b_current_indir = b2[bytenum % 256]; //always starts the same
b_temp[(long)bytenum] = orig[(long)bytenum];
for(long level = 0; level < levels; level++)
{
b_temp[(long)bytenum] ^= b_current_indir;
b_current_indir = b2[(long)b_current_indir];
}
}
memcpy(dataout, b_temp, bytelen);
delete[] b_temp;
}
void Encrypt(_TCHAR* orig, long textlen, BYTE* dataout)
{
long bytelen = textlen * sizeof(_TCHAR);
BYTE* b_temp = new BYTE[bytelen];
#ifdef _DEBUG
ZeroMemory(b_temp, bytelen);
#endif
memcpy(b_temp, orig, bytelen);
for(long bytenum = 0; bytenum < bytelen; bytenum++)
{
BYTE levels = b[(long)(bytenum % 256)],
b_current_indir = b2[(long)(bytenum % 256)];
for(long level = 0; level < levels; level++)
{
b_temp[(long)bytenum] ^= b_current_indir;
b_current_indir = b2[(long)b_current_indir];
}
}
memcpy(dataout, b_temp, bytelen);
delete[] b_temp;
}
int main()
{
LPTSTR testtext = _T("TheMagicBonj");
long textlen = _tcslen(testtext) + 1;
BYTE* b_enc = new BYTE[textlen * sizeof(_TCHAR)];
#ifdef _DEBUG
ZeroMemory(b_enc, textlen * sizeof(_TCHAR));
#endif
Encrypt(testtext, textlen, b_enc);
_TCHAR* t_enc = new _TCHAR[textlen];
ZeroMemory(t_enc, textlen * sizeof(_TCHAR));
memcpy(t_enc, b_enc, textlen * sizeof(_TCHAR));
_tprintf(_T("The encrypted text is \"%s\"\n"), t_enc);
delete[] t_enc;
_TCHAR* t_dec = new _TCHAR[textlen];
Decrypt(b_enc, textlen * sizeof(_TCHAR), t_dec);
_tprintf(_T("The decrypted text is \"%s\"\n"), t_dec);
delete[] t_dec;
}
It seems to work to my eyes, but is it a pile of rubbish?
My initial thoughts were that somebody could crack it by disassembling the
machine code into assembly language, discovering where the global
namespace section of the DLL file was and deriving key1 and key2 from
that, and then just plugging them back through the algorithm, without
necessarily understanding the algorithm from the assembly language. But
even simpler than that, if they discovered that "it was probably *this*
DLL that created *that* encrypted data, so if I find out how to call the
DLL, I can decrypt it". To counter that, I could put the key in the
application very easily. But does this help? Could someone with a
knowledge of assembly language guess with a reasonable degree of accuracy
which bit of data in a PE file was likely to be a key to an encryption
algorithm?
Wouldn't it be just as easy for someone wanting to crack it to still do
that if I had written a DLL that used the Win32 API Crypto functions? If
not, why not? Is the effectiveness of private key cryptography only as
good as how well you can hide the key?
Please give as many thoughts as possible...
My (unsuccessful) attempt to use the Win32 API is as follows:
BOOL GetData(_TCHAR* datain, long lendatain)
{
HCRYPTPROV hCryptProv;
HCRYPTHASH hCryptHash;
HCRYPTKEY hCryptKey;
DWORD bytelen = (lendatain + 10) * sizeof(_TCHAR), databack = 0, bytesback
= 0;
BYTE* bData = new BYTE[bytelen];
_TCHAR* cryptdata = new _TCHAR[lendatain],
* decryptdata = new _TCHAR[lendatain];
BOOL bSuccess = CryptAcquireContext(&hCryptProv, keysetname, NULL,
PROV_RSA_FULL, CRYPT_NEWKEYSET);
if(!bSuccess) bSuccess |= CryptAcquireContext(&hCryptProv, keysetname,
NULL, PROV_RSA_FULL, 0);
memcpy(bData, datain, bytelen);
bSuccess &= CryptCreateHash(hCryptProv, CALG_MD5, 0, 0, &hCryptHash);
bSuccess &= CryptHashData(hCryptHash, (BYTE*)textkey, _tcslen(textkey),
0);
bSuccess &= CryptDeriveKey(hCryptProv, CALG_RC2, hCryptHash, 0,
&hCryptKey);
bSuccess &= CryptEncrypt(hCryptKey, hCryptHash, TRUE, 0, bData,
&bytesback, bytelen);
CryptDestroyKey(hCryptKey);
CryptDestroyHash(hCryptHash);
CryptReleaseContext(hCryptProv, 0);
memcpy(cryptdata, bData, bytesback);
_tprintf(_T("The encrypted data is \"%s\"\n"), cryptdata);
bSuccess = CryptAcquireContext(&hCryptProv, keysetname, NULL,
PROV_RSA_FULL, CRYPT_NEWKEYSET);
if(!bSuccess) bSuccess |= CryptAcquireContext(&hCryptProv, keysetname,
NULL, PROV_RSA_FULL, 0);
bSuccess &= CryptCreateHash(hCryptProv, CALG_MD5, 0, 0, &hCryptHash);
bSuccess &= CryptHashData(hCryptHash, (BYTE*)textkey, _tcslen(textkey),
0);
bSuccess &= CryptDeriveKey(hCryptProv, CALG_RC2, hCryptHash, 0,
&hCryptKey);
bSuccess &= CryptDecrypt(hCryptKey, hCryptHash, TRUE, 0, bData,
&bytesback);
memcpy(decryptdata, bData, bytesback);
databack = (DWORD)(bytelen / sizeof(_TCHAR));
_tprintf(_T("The decrypted data is \"%s\"\n"), decryptdata);
CryptDestroyKey(hCryptKey);
CryptDestroyHash(hCryptHash);
CryptReleaseContext(hCryptProv, 0);
delete[] bData;
delete[] cryptdata;
delete[] decryptdata;
CryptDestroyKey(hCryptKey);
CryptDestroyHash(hCryptHash);
CryptReleaseContext(hCryptProv, 0);
return bSuccess;
}
void test(_TCHAR* string)
{
GetData(string, _tcslen(string));
}
int _tmain()
{
test(_T("TheMagicBonj"));
return 0;
}
The output is:
The encrypted data is "^ðnÍ&lÕeMagicBo"
The decrypted data is "@2"
which is disappointing.