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 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 Code Obfuscation on the zZipLib website.
You want stronger obfuscation?
The zZipLib website offers a technique to provide stronger obfuscation. Implementing the stronger method will be left as an exercise for the reader.
Table of contents
Header ObfuscatedZip.h
/* * 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
Source ObfuscatedZip.cpp
/* * 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; } }
Usage
1) Somewhere in your application header declare:
// Obfuscated Zip ObfuscatedZipFactory *mObfuscatedZipFactory;
2) Somewhere after your Ogre::Root is created:
// Create ObfuscatedZipFactory. mObfuscatedZipFactory = new ObfuscatedZipFactory(); Ogre::ArchiveManager::getSingleton().addArchiveFactory(mObfuscatedZipFactory);
3) To clean-up, after Ogre::Root is deleted:
// Need to delete mObfuscatedZipFactory after Ogre is destroyed otherwise // Ogre::ArchiveFactory may be destroyed prematurely. if(mObfuscatedZipFactory != NULL) { delete mObfuscatedZipFactory; mObfuscatedZipFactory = 0; }