Protecting Art Assets With Encryption
by Pat Wilson · 02/27/2005 (12:36 am) · 24 comments
Download Code File
Why protect assets
Your art is just as valuable as your code, and in some cases, more valuable. Putting all your art out for people to look at, rip off, or modify may not be something you want to do. This will help give you an idea about how to protect your art resources.
CryptoStream
Almost all I/O in the engine is done through the Stream class. This is an optimal entry point for the encryption. The first thing I did was to create a subclass of Stream called CryptoStream.
Overview of CryptoStream
The goal of this class is to take another Stream, and wrap around it, providing encrypted I/O regardless of what kind of stream it is. Memory stream, file stream, network stream, it doesn't matter. As you can see there were a few methods overridden to point at the wrapped stream, and two pure-virtual methods added, 'encrypt' and 'decrypt'. This is a pure-virtual class, and so we need to subclass it to make use of it.
XOR Encryption
The encryption I chose to use for the purposes of this document is a very, very, simple 8-bit XOR encryption. This is not something you want to use to protect your assets. This is an example only. It will work against the casual user, but if someone says, "Oh I bet they just used that encryption resource," and an 8-bit key can be brute forced in less than a second. Now that that is out of the way, the XORStream class:
Using a CryptoStream
Now, how to go about using this class? I chose to add encryption to PNG files because it is very easy and will demonstrate the basics of how to use this in other parts of the engine. The general format for adding it to somewhere in the engine is this:
1. Read the header of a file. If the header is ok, continue like normal.
2. If the header is not ok, create the appropriate CryptoStream, re-read the header.
3. If the header is now ok, you have an encrypted asset, since the de-crypted header was ok.
4. Assign the CryptoStream pointer to the default pointer, so the rest of the read function will read out of the CryptoStream instead.
And that's about it. How does this translate into reading PNGs? Look at GBitmap::readPNG()
First store the starting stream position
Next test the PNG header
A bit later in the PNG code it will set a global PNG read stream, if this is a encrypted PNG, we should set it to the XORStream instead:
Finally, before the function returns, but after all reading is done, clean up memory
Finishing Up
So you may be asking, "Well that's all well and good, but how do I MAKE an encrypted PNG?" I included a very small console app for encrypting/decrypting files in the zip file with the code. Just compile that and you should be good to go. You can also write encrypted streams from inside Torque with a CryptoStream should you want to do that, as well.
Why protect assets
Your art is just as valuable as your code, and in some cases, more valuable. Putting all your art out for people to look at, rip off, or modify may not be something you want to do. This will help give you an idea about how to protect your art resources.
CryptoStream
Almost all I/O in the engine is done through the Stream class. This is an optimal entry point for the encryption. The first thing I did was to create a subclass of Stream called CryptoStream.
class CryptoStream : public Stream
{
typedef Stream Parent;
protected:
Stream *mWrappedStream;
virtual bool _read( const U32 in_numBytes, void *out_pBuffer );
virtual bool _write( const U32 in_numBytes, const void *in_pBuffer );
virtual bool encrypt( const U32 size, const void *in, void *out ) = 0;
virtual bool decrypt( const U32 size, const void *in, void *out ) = 0;
public:
CryptoStream( Stream *wrapstream ) : mWrappedStream( wrapstream ) { setStatus( mWrappedStream->getStatus() ); };
virtual Stream::Status getStatus() const { return mWrappedStream->getStatus(); };
virtual bool hasCapability( const Capability caps ) const { return mWrappedStream->hasCapability( caps ); };
virtual U32 getPosition() const { return mWrappedStream->getPosition(); };
virtual bool setPosition( const U32 position ) { return mWrappedStream->setPosition( position ); };
virtual U32 getStreamSize() { return mWrappedStream->getStreamSize(); };
};Overview of CryptoStream
The goal of this class is to take another Stream, and wrap around it, providing encrypted I/O regardless of what kind of stream it is. Memory stream, file stream, network stream, it doesn't matter. As you can see there were a few methods overridden to point at the wrapped stream, and two pure-virtual methods added, 'encrypt' and 'decrypt'. This is a pure-virtual class, and so we need to subclass it to make use of it.
XOR Encryption
The encryption I chose to use for the purposes of this document is a very, very, simple 8-bit XOR encryption. This is not something you want to use to protect your assets. This is an example only. It will work against the casual user, but if someone says, "Oh I bet they just used that encryption resource," and an 8-bit key can be brute forced in less than a second. Now that that is out of the way, the XORStream class:
class XORStream : public CryptoStream
{
typedef CryptoStream Parent;
protected:
U8 mKey;
virtual bool encrypt( const U32 size, const void *in, void *out );
virtual bool decrypt( const U32 size, const void *in, void *out );
public:
XORStream( Stream *stream, U8 key ) : mKey( key ), Parent( stream ) {};
};
//------------------------------------------------------------------------------
bool XORStream::encrypt( const U32 size, const void *in, void *out )
{
for( int i = 0; i < size; i++ )
((U8 *)out)[i] = ((U8 *)in)[i] ^ mKey;
return true;
}
//------------------------------------------------------------------------------
bool XORStream::decrypt( const U32 size, const void *in, void *out )
{
return encrypt( size, in, out );
}Using a CryptoStream
Now, how to go about using this class? I chose to add encryption to PNG files because it is very easy and will demonstrate the basics of how to use this in other parts of the engine. The general format for adding it to somewhere in the engine is this:
1. Read the header of a file. If the header is ok, continue like normal.
2. If the header is not ok, create the appropriate CryptoStream, re-read the header.
3. If the header is now ok, you have an encrypted asset, since the de-crypted header was ok.
4. Assign the CryptoStream pointer to the default pointer, so the rest of the read function will read out of the CryptoStream instead.
And that's about it. How does this translate into reading PNGs? Look at GBitmap::readPNG()
First store the starting stream position
#ifdef PNGCRYPT U32 startStreamPos = io_rStream.getPosition(); #endif
Next test the PNG header
bool isPng = png_check_sig(header, cs_headerBytesChecked) != 0;
#ifdef PNGCRYPT
XORStream *xstream = NULL;
if( !isPng )
{
// Check to see if it's encrypted
io_rStream.setPosition( startStreamPos );
xstream = new XORStream( &io_rStream, PNGCRYPT_KEY );
// Re-read and re-check the header now
xstream->read(cs_headerBytesChecked, header);
isPng = png_check_sig(header, cs_headerBytesChecked) != 0;
if( !isPng )
{
// Guess not
delete xstream;
xstream = NULL;
}
}
#endifA bit later in the PNG code it will set a global PNG read stream, if this is a encrypted PNG, we should set it to the XORStream instead:
sg_pStream = &io_rStream;
#ifdef PNGCRYPT
if( xstream != NULL )
sg_pStream = xstream;
#endifFinally, before the function returns, but after all reading is done, clean up memory
sg_pStream = NULL;
#ifdef PNGCRYPT
if( xstream != NULL )
delete xstream;
#endifFinishing Up
So you may be asking, "Well that's all well and good, but how do I MAKE an encrypted PNG?" I included a very small console app for encrypting/decrypting files in the zip file with the code. Just compile that and you should be good to go. You can also write encrypted streams from inside Torque with a CryptoStream should you want to do that, as well.
About the author
#2
I'm nowhere near needing to use anything like this at the moment, but I was laying in bed last night thinking about how to protect artwork, as with a 2D game your sprites are the equivalent of 3D models and you need to look after them some how.
The next step is to do something similar with sound files I guess :)
02/27/2005 (3:10 am)
Excellent!I'm nowhere near needing to use anything like this at the moment, but I was laying in bed last night thinking about how to protect artwork, as with a 2D game your sprites are the equivalent of 3D models and you need to look after them some how.
The next step is to do something similar with sound files I guess :)
#3
02/27/2005 (8:23 am)
Sound files? This could work as is on any type of asset. Also it wouldn't be very hard at all to swap out XOR encryption for some more powerful encryption scheme.
#4
Do I understand correctly that to implement this, one just needs to add the files in the zip to the project and re-build? Well, that's what I did and here are the errors:
Torque Demo error LNK2005: "public: bool __thiscall GBitmap::writePNGUncompressed(class Stream &)const " (?writePNGUncompressed@GBitmap@@QBE_NAAVStream@@@Z) already defined in bitmapPng.obj
Torque Demo error LNK2005: "public: bool __thiscall GBitmap::readPNG(class Stream &)" (?readPNG@GBitmap@@QAE_NAAVStream@@@Z) already defined in bitmapPng.obj
Torque Demo error LNK2005: "private: bool __thiscall GBitmap::_writePNG(class Stream &,unsigned int,unsigned int,unsigned int)const " (?_writePNG@GBitmap@@ABE_NAAVStream@@III@Z) already defined in bitmapPng.obj
Torque Demo error LNK2005: "public: bool __thiscall GBitmap::writePNG(class Stream &,bool)const " (?writePNG@GBitmap@@QBE_NAAVStream@@_N@Z) already defined in bitmapPng.obj
Torque Demo fatal error LNK1169: one or more multiply defined symbols found
Can someone try to enlighten me? :)
02/27/2005 (9:53 am)
Thank you for posting this Pat!Do I understand correctly that to implement this, one just needs to add the files in the zip to the project and re-build? Well, that's what I did and here are the errors:
Torque Demo error LNK2005: "public: bool __thiscall GBitmap::writePNGUncompressed(class Stream &)const " (?writePNGUncompressed@GBitmap@@QBE_NAAVStream@@@Z) already defined in bitmapPng.obj
Torque Demo error LNK2005: "public: bool __thiscall GBitmap::readPNG(class Stream &)" (?readPNG@GBitmap@@QAE_NAAVStream@@@Z) already defined in bitmapPng.obj
Torque Demo error LNK2005: "private: bool __thiscall GBitmap::_writePNG(class Stream &,unsigned int,unsigned int,unsigned int)const " (?_writePNG@GBitmap@@ABE_NAAVStream@@III@Z) already defined in bitmapPng.obj
Torque Demo error LNK2005: "public: bool __thiscall GBitmap::writePNG(class Stream &,bool)const " (?writePNG@GBitmap@@QBE_NAAVStream@@_N@Z) already defined in bitmapPng.obj
Torque Demo fatal error LNK1169: one or more multiply defined symbols found
Can someone try to enlighten me? :)
#5
I tried the included console app to encrypt one of my pngs and it changed it into a 1 byte file that obviously doesn't work...any ideas? - thanks!
02/27/2005 (10:19 am)
Ok, I think I fixed it...bitmapPng.cc was already there in the dgl directory, so I just replaced it with the one from the zip and it worked.I tried the included console app to encrypt one of my pngs and it changed it into a 1 byte file that obviously doesn't work...any ideas? - thanks!
#6
New zip file is up there that fixes that. It was late, probably just slipped my mind.
02/27/2005 (11:43 am)
Oops, somehow the wrong version of that .cpp file got in there...New zip file is up there that fixes that. It was late, probably just slipped my mind.
#7
change:
And it works a treat.
EDIT:
*never mind* *hallucination is over*
02/27/2005 (11:59 am)
Just a minor brain fart in the xorcrypt.cpp, Jacob.change:
if( !feof( inputFile ) )
{
fputc( curByte, outputFile );
bytecount++;
break;
}toif( !feof( inputFile ) )
{
fputc( curByte, outputFile );
bytecount++;
}
else
break;And it works a treat.
EDIT:
*never mind* *hallucination is over*
#8
02/27/2005 (12:31 pm)
that's awesome, I was wondering about this issue re: t2d myself
#9
02/27/2005 (12:43 pm)
All works perfectly now - thank you again!
#10
02/28/2005 (6:24 pm)
How about a reverse bamboozle top-hat overture crypto? Do you think anyone could break that?
#11
02/28/2005 (8:12 pm)
The weakness in the BTHO Crypto algorithim is widly known and deals primarly with overflowing your mom.
#12
03/01/2005 (9:52 pm)
No words man... check my rating.
#13
ROTFL. You're Killing me.
04/01/2005 (4:58 pm)
Quote: The weakness in the BTHO Crypto algorithim is widly known and deals primarly with overflowing your mom.
ROTFL. You're Killing me.
#14
Is the key defined in script somewhere with something like: $key=1234;
Sorry for the dense question :)
06/09/2005 (1:24 am)
This is probably a really daft question, but how does it know the key needed to decode the files? It must be passed in from somewhere, but being a code newbie, I don't know where it comes from.Is the key defined in script somewhere with something like: $key=1234;
Sorry for the dense question :)
#15
It seems that your profile doesn't state that you are a SDK owner, so you wouldn't have access to the source to make this work. It is not done in script...Since you are able to view this page and if it let you download the resource, you can look around line 95 in the bitmapPng.cc that comes with the zip but again you need to re-compile the project in order to use the resource or change the key for that matter.
06/09/2005 (10:06 am)
Philip,It seems that your profile doesn't state that you are a SDK owner, so you wouldn't have access to the source to make this work. It is not done in script...Since you are able to view this page and if it let you download the resource, you can look around line 95 in the bitmapPng.cc that comes with the zip but again you need to re-compile the project in order to use the resource or change the key for that matter.
#16
I was just unsure how the key got into the exectuable in order to decrypt the file. Now you've pointed me at it, I can see it.
Thanks.
06/09/2005 (12:27 pm)
I am a T2D owner, but not a TGE owner. I can implement this, and I can successfully recompile T2D with this resource integrated into it.I was just unsure how the key got into the exectuable in order to decrypt the file. Now you've pointed me at it, I can see it.
Thanks.
#17
Thanks for sharing the work Pat!
06/17/2005 (6:04 am)
This is AWESOME! I will definately be using this. Looks like that cryptography class I took might come in handy after all.Thanks for sharing the work Pat!
#18
I would recommend using blowfish (fast, effective and free) or some other strong cypher to transform the data as it is streamed.
Also, I would add a special header (unencrypted) when writing the encrypted stream containing a tag, version and perhaps a hash (with carefully selected salt values) of the key used (this way you can just hash the key passed in and check it agains the header hash, if it matches it is the correct key, else don't try and decrypt with it as the result will be garbage).
I actually wrote exactly this as CryptoStream for my own stuff a few years ago and still use it a lot. It can encrypt/decrypt 100MB in a few seconds (can't remember exact figures). I will port it to torque streams when i get the time and release it as a resource.
To check out blowfish: www.schneier.com/blowfish.html
layder
neo
06/21/2005 (11:33 am)
@Philip Usually the key is passed in by the user, e.g. when they enter a serial number to unlock it or if it is just for locking the data it would be passed in by the app itself. Obviously its not too hard to crack then as the key is right there in the code and can be ripped quite quickly.I would recommend using blowfish (fast, effective and free) or some other strong cypher to transform the data as it is streamed.
Also, I would add a special header (unencrypted) when writing the encrypted stream containing a tag, version and perhaps a hash (with carefully selected salt values) of the key used (this way you can just hash the key passed in and check it agains the header hash, if it matches it is the correct key, else don't try and decrypt with it as the result will be garbage).
I actually wrote exactly this as CryptoStream for my own stuff a few years ago and still use it a lot. It can encrypt/decrypt 100MB in a few seconds (can't remember exact figures). I will port it to torque streams when i get the time and release it as a resource.
To check out blowfish: www.schneier.com/blowfish.html
layder
neo
#19
08/13/2005 (8:04 am)
Hmm but isn't blowfish too strong for U.S. export restrictions? I'd try RSA to be on the safe side. Plus no matter how strong you encrypt someone can still get your media once it's decrypted in memory. One app I think is called "OpenGL texture debugger" ? that does exactly that and claims to work on any OGL program. So this is good and all, but I'd say even XOR is enough to keep honest folks honest. Still a cool resource.
#20
And yes I agree with you if someone wants it, they can get it. I am simple trying to provide another option for not just
protection of assets but for say locking packages based on a serial key which you would only receive on registering.
The locked levels could be cracked using the above method, but I doubt they would crack blowfish (or rsa, etc) without a key
using resources currently available.
So while 'honest' folk might not crack it, they might 'honestly' download a crack written by 'dishonest' folks ;p
~neo
ps: This resource would be handy at the ResourceManager level as well as a transform stream so you could protect
all your data, perhaps allowing selection of cypher etc.
08/13/2005 (8:19 am)
from the blowfish site:Quote:
...It takes a variable-length key, from 32 bits to 448 bits, making it ideal for both domestic and exportable use...
And yes I agree with you if someone wants it, they can get it. I am simple trying to provide another option for not just
protection of assets but for say locking packages based on a serial key which you would only receive on registering.
The locked levels could be cracked using the above method, but I doubt they would crack blowfish (or rsa, etc) without a key
using resources currently available.
So while 'honest' folk might not crack it, they might 'honestly' download a crack written by 'dishonest' folks ;p
~neo
ps: This resource would be handy at the ResourceManager level as well as a transform stream so you could protect
all your data, perhaps allowing selection of cypher etc.

Associate Melv May
Mmm, thinks T2D demos etc.
- Melv.