-
-
Notifications
You must be signed in to change notification settings - Fork 291
Protecting image assets
Tutorial by Rudi Hammad
-
In TexturePacker, we drag and drop the
image/spriteSheetwe want to encrypt. For instanceplayerSprite.png. Now we setTextureFormattoPVR+zlib(.pvr.ccz), and in Content protection menu we generate a Key and click on "Save as globalKey". This allows to get it back at anytime clicking on "Use global key", so all future encrypted images will use. Let's say the generated key is9512f9a50dc075a015f9a745ba60488b.This will generate
playerSprite.pvr.ccz, which is the compressed and encrypted image. Let's store that images inContent/myAssets/playerSprite.pvr.ccz.NOTE:
playerSprite.pngshould be removed fromContentbecause we don't want to ship it with the game. -
In the
AppDelegate.cppfile, we spread the key in 4 part parts usingZipUtils::setPvrEncryptionKeyPart(...)
bool AppDelegate::applicationDidFinishLaunching()
{
ZipUtils::setPvrEncryptionKeyPart(0, 0x9512f9a5);
// initialize director
auto director = Director::getInstance();
//
//... more code
// ...
ZipUtils::setPvrEncryptionKeyPart(1, 0x0dc075a0);
//
//... more code
// ...
ZipUtils::setPvrEncryptionKeyPart(2, 0x415f9a745);
//
// ... more code
// ...
ZipUtils::setPvrEncryptionKeyPart(3, 0xba60488b);
return true;
}
- In our game scene init.
bool MyScene::init()
{
// ...
auto sprite = Sprite::create("myAssets/playerSprite.pvr.ccz");
sprite->setPosition(m_visibleSize/2);
addChild(sprite);
// ...
}
For tile maps we start working as usual without any encryption. So we have for instance in Content/myAssets:
- myMap.tmx <-- main Tiled map.
- surfaceTileset.tsx <-- referenced by myMap.tmx at <tileset firstgid="1" source="surfaceTileset.tsx"/>
- myTilset.png <-- referencef by surfaceTileset at <image source="myTilset.png" width="120" height="120"/>
Once our level is done, we encrypt myTilset.png in TexturePacker as we did before. The result being myTilset.pvr.ccz.
At this point we should replace myTileset.png with myTilset.pvr.ccz and edit surfaceTileset.tsx to reference that images
<image source="myTilset.pvr.ccz" width="120" height="120"/>.
Again, myTileset.png should be remove from Content so it doesn't ship with the game.
Now all we have to do is:
auto map = TMXTiledMap::create("myAssets/myMap.tmx");
addChild(map);
With the examples above, this what we ship when release the game
Content
|_ myAssets
-myMap.tmx
-surfaceTileset.tsx
-myTilset.pvr.ccz
-playerSprite.pvr.ccz
- etc...
So what did we achieve with this workflow?
- We encrypted images so they can't be accessed by casual users when they look at the Content folder of the game.
- The tilemap can be openned in tiled but it will be missing all the tiles since it can't display encrypted images.
This one protection layer that will work against most of the users trying to access the content. We can add another protector layer with XOR, but IMPORTANT: THIS WON'T WOKR FOR TILEMAPS, ONLY SPRITES.
The first thing we need to do take each .png and encrypt it as .pvr.ccz as we did before and bundle them in a zip file. For instance:
Content
|_ Assets_world_1.zip
|_ cityBackgound.pvr.ccz
|_ enemy.pvr.ccz
|_ ...
Now we need to XOR that zip. For this we can use this code in a new empty Visual Studio project:
#include <chrono>
#include <fstream>
#include <iostream>
#include <vector>
#include <filesystem>
constexpr unsigned char KEY = 0x5A; // Arbitrary key
int main()
{
auto start = std::chrono::high_resolution_clock::now();
std::cout << "Current directory: " << std::filesystem::current_path() << '\n';
// Open the original ZIP
std::ifstream input("../../Content/Assets_world_1.zip", std::ios::binary);
if (!input) {
return 1;
}
// Read the entire ZIP file into memory
std::vector<unsigned char> data( (std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
// XOR the entire archive
for (auto& byte : data) {
byte ^= KEY;
}
// Write the obfuscated archive
std::ofstream output("../../Content/Assets_world_1.data", std::ios::binary);
if (!output) {
return 1;
}
output.write(reinterpret_cast<const char*>(data.data()), data.size());
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Processed " << data.size() << " bytes in " << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << " us\n";
return 0;
}
Running this will create Content/Assets_world_1.data. Now we can delete Content/Assets_world_1.zip to avoid shipping it with the game.
(NOTE: the input/output path has ../../ because in this case the Visual Studio project, called EncryptZip.sln, is in a folder called EncryptZip in the main Axmol game directory, so we need to step out to levels to access Content.)
The next step is to decrypt the .data. So in our game scene init we'll do:
// Decrypt the encrypted .data
auto encryptedData = FileUtils::getInstance()->getDataFromFile("Assets_world_1/encryptedTileMapAssets.data");
constexpr unsigned char KEY = 0x5A; // Same key set in the "stand alone" encryption project
Data decryptedData = encryptedData; // Copy the decrypted data to preserve the original
for (size_t i = 0; i < decryptedData.size(); ++i) {
decryptedData.data()[i] ^= KEY;
}
// Create the zip file from the decrypted data.
auto zip = ax::ZipFile::createWithData(std::move(decryptedData));
// Create a ResizableBufferAdapter that we can reuse to extract the data
std::vector<unsigned char> dataBuffer;
ax::ResizableBufferAdapter<std::vector<unsigned char>>dataAdapter(&dataBuffer);
// Create the sprite
if (!zip->getFileData("cityBackgound.pvr.ccz", &dataAdapter)) { // notice the image it self remains encrypted as .pvr.ccz
AXLOG("ERROR AT DECRYPTION"); // This provided dataBuffer with the Byte data related to that image
}
auto image = std::make_unique<Image>();
image->initWithImageData(dataBuffer.data(), dataBuffer.size()); // Now we can get that image from the the buffer, create a texture from it
auto imageTexture = std::make_unique<Texture2D>(); // and therefor create the sprite Sprite::createWithTexture(...)
imageTexture->initWithImage(image.get());
imageTexture->setAliasTexParameters();
auto sprite = Sprite::createWithTexture(imageTexture.get());
sprite->setPosition(Director::getInstance()->getVisibleSize()/2);
addChild(sprite);
With this worflow we have added a second layer of security, so now a hacker would need to reverse engineer multiple things:
first, decrypt the XOR .data, and if they succeed, they would need also to decrypt the pvr.ccz images that are protected
with the key splitted in 4 parts.
About the tilemaps: unfortunately, we can't XOR the tilemaps because the xml can't read from memory the source files
they need. So this is an example of how our Content would look like:
Content
|_ Assets_world_1.zip
|_ cityBackgound.pvr.ccz
|_ enemy.pvr.ccz
|_ ...
|_ myAssets
|_myMap.tmx
|_surfaceTileset.tsx
|_myTilset.pvr.ccz
Where the first block, Assets_world_1, has the two layers of protection as we just saw: pvr.ccz encryption + XOR.
And the second block, myAssets, one layer of protection: pvr.ccz encryption.
- Cocos2d-x Migration Guide
- SpriteKit to Axmol Engine
- Update Guide to 2.3.0+ for Android
- Migration Guide for PR 3173: Refactor InputSystem