History: Obfuscated Zip
Source of version: 3 (current)
Copy to clipboard
{maketoc}
!!General
Many content providers require that licensed assets are packaged into a protected form when shipped with your application. An obfuscated zip file is one way to pack up your application resources in a way that prevents a casual user from accessing them. The method uses a 'magic number' and a simple xor to make the file unreadable. The classes presented with this code snippet abstracts an Ogre::Archive (almost an exact copy of Ogre::ZipArchive) and seamlessly hooks into the Ogre::ArchiveManager to handle decoding and reading directly from an obfuscated zip file.
__Why not password protect the zip file?__
The zZipLib website provides a good [http://http://zziplib.sourceforge.net/zzip-crypt.html|rational] for why password protection in a zip file is weak. A casual user can download a cracking tool from any number of websites and effortlessly open your zip file.
__Isn't an xor'd zip file easily cracked?__
A user with a more advanced level of skill can guess (or write a program to guess) the 'magic number' and un-obfuscate the zip file. The key advantage to the xor technique is that there are no ready-made tools available to de-obfuscate the file (at least none that I'm aware of). For information about the xor method used in this code snippet see [http://http://zziplib.sourceforge.net/zzip-xor.html|Code Obfuscation on the zZipLib website].
__You want stronger obfuscation?__
The zZipLib website offers a technique to provide [http://http://zziplib.sourceforge.net/zzip-cryptoid.html|stronger obfuscation]. Implementing the stronger method will be left as an exercise for the reader.
!!Header ObfuscatedZip.h
{CODE(wrap="1", colors="c++")}
/*
* ObfuscatedZip.h
*/
#ifndef obfuscated_zip_h
#define obfuscated_zip_h
#include "OgreArchive.h"
#include "OgreArchiveFactory.h"
// Forward declaration for zziplib to avoid header file dependency.
typedef struct zzip_dir ZZIP_DIR;
typedef struct zzip_file ZZIP_FILE;
namespace MyNameSpace
{
/** Specialisation of the Archive class to allow reading of files from an
obfuscated zip format source archive.
@remarks
This archive format supports obfuscated zip archives.
*/
class ObfuscatedZip : public Ogre::Archive
{
protected:
/// Handle to root zip file
ZZIP_DIR* mZzipDir;
/// Handle any errors from zzip
void checkZzipError(int zzipError, const Ogre::String& operation) const;
/// File list (since zziplib seems to only allow scanning of dir tree once)
Ogre::FileInfoList mFileList;
public:
ObfuscatedZip(const Ogre::String& name, const Ogre::String& archType );
~ObfuscatedZip();
/// @copydoc Archive::isCaseSensitive
bool isCaseSensitive(void) const { return false; }
/// @copydoc Archive::load
void load();
/// @copydoc Archive::unload
void unload();
/// @copydoc Archive::open
Ogre::DataStreamPtr open(const Ogre::String& filename) const;
/// @copydoc Archive::list
Ogre::StringVectorPtr list(bool recursive = true, bool dirs = false);
/// @copydoc Archive::listFileInfo
Ogre::FileInfoListPtr listFileInfo(bool recursive = true, bool dirs = false);
/// @copydoc Archive::find
Ogre::StringVectorPtr find(const Ogre::String& pattern, bool recursive = true,
bool dirs = false);
/// @copydoc Archive::findFileInfo
Ogre::FileInfoListPtr findFileInfo(const Ogre::String& pattern, bool recursive = true,
bool dirs = false);
/// @copydoc Archive::exists
bool exists(const Ogre::String& filename);
/// @copydoc Archive::getModifiedTime
time_t getModifiedTime(const Ogre::String& filename);
};
/** Specialisation of ArchiveFactory for Obfuscated Zip files. */
class ObfuscatedZipFactory : public Ogre::ArchiveFactory
{
public:
virtual ~ObfuscatedZipFactory() {}
/// @copydoc FactoryObj::getType
const Ogre::String& getType(void) const;
/// @copydoc FactoryObj::createInstance
Ogre::Archive *createInstance( const Ogre::String& name )
{
// Change "OBFUSZIP" to match the file extension of your choosing.
return new ObfuscatedZip(name, "OBFUSZIP");
}
/// @copydoc FactoryObj::destroyInstance
void destroyInstance( Ogre::Archive* arch) { OGRE_DELETE arch; }
};
/** Specialisation of DataStream to handle streaming data from zip archives. */
class ObfuscatedZipDataStream : public Ogre::DataStream
{
protected:
ZZIP_FILE* mZzipFile;
public:
/// Unnamed constructor
ObfuscatedZipDataStream(ZZIP_FILE* zzipFile, size_t uncompressedSize);
/// Constructor for creating named streams
ObfuscatedZipDataStream(const Ogre::String& name, ZZIP_FILE* zzipFile, size_t uncompressedSize);
~ObfuscatedZipDataStream();
/// @copydoc DataStream::read
size_t read(void* buf, size_t count);
/// @copydoc DataStream::skip
void skip(long count);
/// @copydoc DataStream::seek
void seek( size_t pos );
/// @copydoc DataStream::seek
size_t tell(void) const;
/// @copydoc DataStream::eof
bool eof(void) const;
/// @copydoc DataStream::close
void close(void);
};
}
#endif
{CODE}
!!Source ObfuscatedZip.cpp
{CODE(wrap="1", colors="c++")}
/*
* ObfuscatedZip.cpp
*/
#include "ObfuscatedZip.h"
#include <zzip/zzip.h>
#include <zzip/plugin.h>
namespace MyNameSpace
{
// Change this magic number to a value of your choosing.
static const int xor_value = 0x10;
static zzip_plugin_io_handlers xor_handlers = { };
// Change "OBFUSZIP" to match the file extension of your choosing.
static zzip_strings_t xor_fileext[] = { ".OBFUSZIP", 0 };
// Static method that un-obfuscates an obfuscated file.
static zzip_ssize_t xor_read (int fd, void* buf, zzip_size_t len)
{
const zzip_ssize_t bytes = read(fd, buf, len);
zzip_ssize_t i;
char* pch = (char*)buf;
for (i=0; i<bytes; ++i)
{
pch[i] ^= xor_value;
}
return bytes;
}
/// Utility method to format out zzip errors
Ogre::String getZzipErrorDescription(zzip_error_t zzipError)
{
Ogre::String errorMsg;
switch (zzipError)
{
case ZZIP_NO_ERROR:
break;
case ZZIP_OUTOFMEM:
errorMsg = "Out of memory.";
break;
case ZZIP_DIR_OPEN:
case ZZIP_DIR_STAT:
case ZZIP_DIR_SEEK:
case ZZIP_DIR_READ:
errorMsg = "Unable to read zip file.";
break;
case ZZIP_UNSUPP_COMPR:
errorMsg = "Unsupported compression format.";
break;
case ZZIP_CORRUPTED:
errorMsg = "Corrupted archive.";
break;
default:
errorMsg = "Unknown error.";
break;
};
return errorMsg;
}
//-----------------------------------------------------------------------
ObfuscatedZip::ObfuscatedZip(const Ogre::String& name, const Ogre::String& archType )
: Archive(name, archType), mZzipDir(0)
{
zzip_init_io(&xor_handlers, 0);
xor_handlers.fd.read = &xor_read;
}
//-----------------------------------------------------------------------
ObfuscatedZip::~ObfuscatedZip()
{
unload();
}
//-----------------------------------------------------------------------
void ObfuscatedZip::load()
{
if (!mZzipDir)
{
zzip_error_t zzipError;
mZzipDir = zzip_dir_open_ext_io(mName.c_str(), &zzipError, xor_fileext, &xor_handlers);
checkZzipError(zzipError, "opening OBFUSZIP file");
// Cache names
ZZIP_DIRENT zzipEntry;
while (zzip_dir_read(mZzipDir, &zzipEntry))
{
Ogre::FileInfo info;
info.archive = this;
// Get basename / path
Ogre::StringUtil::splitFilename(zzipEntry.d_name, info.basename, info.path);
info.filename = zzipEntry.d_name;
// Get sizes
info.compressedSize = static_cast<size_t>(zzipEntry.d_csize);
info.uncompressedSize = static_cast<size_t>(zzipEntry.st_size);
// folder entries
if (info.basename.empty())
{
info.filename = info.filename.substr (0, info.filename.length () - 1);
Ogre::StringUtil::splitFilename(info.filename, info.basename, info.path);
// Set compressed size to -1 for folders; anyway nobody will check
// the compressed size of a folder, and if he does, its useless anyway
info.compressedSize = size_t (-1);
}
mFileList.push_back(info);
}
}
}
//-----------------------------------------------------------------------
void ObfuscatedZip::unload()
{
if (mZzipDir)
{
zzip_dir_close(mZzipDir);
mZzipDir = 0;
mFileList.clear();
}
}
//-----------------------------------------------------------------------
Ogre::DataStreamPtr ObfuscatedZip::open(const Ogre::String& filename) const
{
// Format not used here (always binary)
ZZIP_FILE* zzipFile =
zzip_file_open(mZzipDir, filename.c_str(), ZZIP_ONLYZIP | ZZIP_CASELESS);
if (!zzipFile)
{
int zerr = zzip_error(mZzipDir);
Ogre::String zzDesc = getZzipErrorDescription((zzip_error_t)zerr);
Ogre::LogManager::getSingleton().logMessage(
mName + " - Unable to open file " + filename + ", error was '" + zzDesc + "'");
// return null pointer
return Ogre::DataStreamPtr();
}
// Get uncompressed size too
ZZIP_STAT zstat;
zzip_dir_stat(mZzipDir, filename.c_str(), &zstat, ZZIP_CASEINSENSITIVE);
// Construct & return stream
return Ogre::DataStreamPtr(OGRE_NEW ObfuscatedZipDataStream(filename, zzipFile, static_cast<size_t>(zstat.st_size)));
}
//-----------------------------------------------------------------------
Ogre::StringVectorPtr ObfuscatedZip::list(bool recursive, bool dirs)
{
Ogre::StringVectorPtr ret = Ogre::StringVectorPtr(OGRE_NEW_T(Ogre::StringVector, Ogre::MEMCATEGORY_GENERAL)(), Ogre::SPFM_DELETE_T);
Ogre::FileInfoList::iterator i, iend;
iend = mFileList.end();
for (i = mFileList.begin(); i != iend; ++i)
if ((dirs == (i->compressedSize == size_t (-1))) &&
(recursive || i->path.empty()))
ret->push_back(i->filename);
return ret;
}
//-----------------------------------------------------------------------
Ogre::FileInfoListPtr ObfuscatedZip::listFileInfo(bool recursive, bool dirs)
{
Ogre::FileInfoList* fil = OGRE_NEW_T(Ogre::FileInfoList, Ogre::MEMCATEGORY_GENERAL)();
Ogre::FileInfoList::const_iterator i, iend;
iend = mFileList.end();
for (i = mFileList.begin(); i != iend; ++i)
if ((dirs == (i->compressedSize == size_t (-1))) &&
(recursive || i->path.empty()))
fil->push_back(*i);
return Ogre::FileInfoListPtr(fil, Ogre::SPFM_DELETE_T);
}
//-----------------------------------------------------------------------
Ogre::StringVectorPtr ObfuscatedZip::find(const Ogre::String& pattern, bool recursive, bool dirs)
{
Ogre::StringVectorPtr ret = Ogre::StringVectorPtr(OGRE_NEW_T(Ogre::StringVector, Ogre::MEMCATEGORY_GENERAL)(), Ogre::SPFM_DELETE_T);
// If pattern contains a directory name, do a full match
bool full_match = (pattern.find ('/') != Ogre::String::npos) ||
(pattern.find ('\\') != Ogre::String::npos);
Ogre::FileInfoList::iterator i, iend;
iend = mFileList.end();
for (i = mFileList.begin(); i != iend; ++i)
if ((dirs == (i->compressedSize == size_t (-1))) &&
(recursive || full_match || i->path.empty()))
// Check basename matches pattern (zip is case insensitive)
if (Ogre::StringUtil::match(full_match ? i->filename : i->basename, pattern, false))
ret->push_back(i->filename);
return ret;
}
//-----------------------------------------------------------------------
Ogre::FileInfoListPtr ObfuscatedZip::findFileInfo(const Ogre::String& pattern,
bool recursive, bool dirs)
{
Ogre::FileInfoListPtr ret = Ogre::FileInfoListPtr(OGRE_NEW_T(Ogre::FileInfoList, Ogre::MEMCATEGORY_GENERAL)(), Ogre::SPFM_DELETE_T);
// If pattern contains a directory name, do a full match
bool full_match = (pattern.find ('/') != Ogre::String::npos) ||
(pattern.find ('\\') != Ogre::String::npos);
Ogre::FileInfoList::iterator i, iend;
iend = mFileList.end();
for (i = mFileList.begin(); i != iend; ++i)
if ((dirs == (i->compressedSize == size_t (-1))) &&
(recursive || full_match || i->path.empty()))
// Check name matches pattern (zip is case insensitive)
if (Ogre::StringUtil::match(full_match ? i->filename : i->basename, pattern, false))
ret->push_back(*i);
return ret;
}
//-----------------------------------------------------------------------
bool ObfuscatedZip::exists(const Ogre::String& filename)
{
ZZIP_STAT zstat;
int res = zzip_dir_stat(mZzipDir, filename.c_str(), &zstat, ZZIP_CASEINSENSITIVE);
return (res == ZZIP_NO_ERROR);
}
//---------------------------------------------------------------------
time_t ObfuscatedZip::getModifiedTime(const Ogre::String& filename)
{
// Zziplib doesn't yet support getting the modification time of individual files
// so just check the mod time of the zip itself
struct stat tagStat;
bool ret = (stat(mName.c_str(), &tagStat) == 0);
if (ret)
{
return tagStat.st_mtime;
}
else
{
return 0;
}
}
//-----------------------------------------------------------------------
void ObfuscatedZip::checkZzipError(int zzipError, const Ogre::String& operation) const
{
if (zzipError != ZZIP_NO_ERROR)
{
Ogre::String errorMsg = getZzipErrorDescription(static_cast<zzip_error_t>(zzipError));
OGRE_EXCEPT(Ogre::Exception::ERR_INTERNAL_ERROR,
mName + " - error whilst " + operation + ": " + errorMsg,
"ObfuscatedZip::checkZzipError");
}
}
//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
ObfuscatedZipDataStream::ObfuscatedZipDataStream(ZZIP_FILE* zzipFile, size_t uncompressedSize)
: mZzipFile(zzipFile)
{
mSize = uncompressedSize;
}
//-----------------------------------------------------------------------
ObfuscatedZipDataStream::ObfuscatedZipDataStream(const Ogre::String& name, ZZIP_FILE* zzipFile, size_t uncompressedSize)
:DataStream(name), mZzipFile(zzipFile)
{
mSize = uncompressedSize;
}
//-----------------------------------------------------------------------
ObfuscatedZipDataStream::~ObfuscatedZipDataStream()
{
close();
}
//-----------------------------------------------------------------------
size_t ObfuscatedZipDataStream::read(void* buf, size_t count)
{
zzip_ssize_t r = zzip_file_read(mZzipFile, (char*)buf, count);
if (r<0) {
ZZIP_DIR *dir = zzip_dirhandle(mZzipFile);
Ogre::String msg = zzip_strerror_of(dir);
OGRE_EXCEPT(Ogre::Exception::ERR_INTERNAL_ERROR,
mName+" - error from zziplib: "+msg,
"ObfuscatedZipDataStream::read");
}
return (size_t) r;
}
//-----------------------------------------------------------------------
void ObfuscatedZipDataStream::skip(long count)
{
zzip_seek(mZzipFile, static_cast<zzip_off_t>(count), SEEK_CUR);
}
//-----------------------------------------------------------------------
void ObfuscatedZipDataStream::seek( size_t pos )
{
zzip_seek(mZzipFile, static_cast<zzip_off_t>(pos), SEEK_SET);
}
//-----------------------------------------------------------------------
size_t ObfuscatedZipDataStream::tell(void) const
{
return zzip_tell(mZzipFile);
}
//-----------------------------------------------------------------------
bool ObfuscatedZipDataStream::eof(void) const
{
return (zzip_tell(mZzipFile) >= static_cast<zzip_off_t>(mSize));
}
//-----------------------------------------------------------------------
void ObfuscatedZipDataStream::close(void)
{
if (mZzipFile != 0)
{
zzip_file_close(mZzipFile);
mZzipFile = 0;
}
}
//-----------------------------------------------------------------------
const Ogre::String& ObfuscatedZipFactory::getType(void) const
{
static Ogre::String name = "OBFUSZIP";
return name;
}
}
{CODE}
!!Usage
1) Somewhere in your application header declare:
{CODE(wrap="1", colors="c++")}// Obfuscated Zip
ObfuscatedZipFactory *mObfuscatedZipFactory;{CODE}
2) Somewhere after your Ogre::Root is created:
{CODE(wrap="1", colors="c++")}// Create ObfuscatedZipFactory.
mObfuscatedZipFactory = new ObfuscatedZipFactory();
Ogre::ArchiveManager::getSingleton().addArchiveFactory(mObfuscatedZipFactory);{CODE}
3) To clean-up, __after Ogre::Root is deleted__:
{CODE(wrap="1", colors="c++")}// Need to delete mObfuscatedZipFactory after Ogre is destroyed otherwise
// Ogre::ArchiveFactory may be destroyed prematurely.
if(mObfuscatedZipFactory != NULL)
{
delete mObfuscatedZipFactory;
mObfuscatedZipFactory = 0;
}{CODE}
!!How to obfuscate your zip file
The following command line utility will obfuscate your zip file using your chosen magic number.
{REMARKSBOX(type="warning",title="WARNING",highlight="y",close="no",width="275px")}Make sure all of the magic numbers match.{REMARKSBOX}
{CODE(wrap="1", colors="c++")}
#include <string>
#include <zzip/plugin.h>
// Your magic number. Change this to match the magic number in your un-obfuscator.
static int xor_value = 0x10;
// Override the read handler for zzip.
static zzip_ssize_t our_read(int fd, void* buf, zzip_size_t len)
{
const zzip_ssize_t bytes = read(fd, buf, len);
zzip_ssize_t i;
char* pch = (char*)buf;
for (i=0; i<bytes; ++i)
{
pch[i] ^= xor_value;
}
return bytes;
}
int main (int argc, char* argv[])
{
std::string usage =
" Obfuscate <fileName>\n"
" - This program will (un)obfuscate a (.OBFUSZIP).zip file.\n"
" Feed it a .zip file and it will obfuscate it.\n"
" Feed it a .OBFUSZIP file and it will unobfuscate it.\n";
if(argc != 2)
{
printf(usage.c_str());
return 0;
}
if(strlen(argv[1]) > 128)
{
fprintf(stderr, "Please provide a filename shorter than 128 chars.\n");
exit(1);
}
std::string fileName_ext = argv[1];
std::string outFileName;
// Get the extension and filename
size_t delim = fileName_ext.find_last_of(".");
std::string fileName = fileName_ext.substr(0 , delim);
std::string extension = fileName_ext.substr(delim + 1);
// Convert the extension to lower case.
for(size_t position = 0; position < extension.size(); ++position)
{
extension[position] = tolower(extension[position]);
}
if(extension == "zip")
{
outFileName = fileName + ".OBFUSZIP";
}
else if(extension == "obfuszip")
{
outFileName = fileName + ".zip";
}
else
{
printf(usage.c_str());
return 0;
}
/* Convert the file */
FILE* fin = fopen(fileName_ext.c_str(), "rb");
if(!fin)
{
fprintf(stderr, "Can't open input file \"%s\"\n", fileName_ext.c_str());
exit(1);
}
FILE* fout = fopen(outFileName.c_str(), "wb");
if(!fout)
{
fprintf(stderr, "Can't open output file \"%s\"\n", outFileName.c_str());
exit(1);
}
int ch;
while((ch = fgetc(fin)) != EOF)
{
ch ^= xor_value;
fputc(ch, fout);
}
fclose(fout);
fclose(fin);
return 0;
}
{CODE}
{REMARKSBOX(type="note",title="Note",highlight="y",close="n",width="275px")}The command line utility above has been compiled with MinGW and tested on WinXP. Other platforms and other compilers have not been tested.{REMARKSBOX}