Obfuscated Zip I/O        

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.


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;
}