Safeguarding your assets from getting stolen

Discussion in 'RPG Maker MV Tutorials' started by Poryg, Apr 9, 2018.

  1. Poryg

    Poryg Pixie of the Emvee kingdom, Ham of a Hamster Veteran

    Messages:
    3,528
    Likes Received:
    8,677
    Location:
    Czech Republic
    First Language:
    Czech
    Primarily Uses:
    RMMV
    I don't think it's unknown that MV files are badly protected. MV editor allows image and audio encryption, however it's so weak that it's very easy to break through it. And due to the nature of MV code it's open for edits and vulnerable to code injections (as we could see with the episode where a hacker injected miner malware into stolen games and uploaded them on itch.io).
    I don't think you like it and neither do I. After all, nobody likes leaving their door open for a burglar to just enter and rob them. So I have invested a lot of time into improving the security of MV resources. I've gone through so many options even going through other languages (C++ is a very viable option, but contains many problems including the fact that coding in it is very tedious and I'd have to recreate all MV engine just for C++). And finally I'm ready to share the results of my research with you.

    Before I start though, I have to say one thing:

    What we need:
    A text editor that can handle JS well (I use Sublime text 2), encryption/compression scripts of our choice (I use crypto.js), PIXI-sound (possibly) and nw.js SDK. And of course MV 1.6.0 or more recent (although even if you don't have it, you may find some interesting ideas here). Why that? I'll explain later.

    Important notes:
    This is an advanced tutorial. Javascript knowledge of a certain level is absolutely mandatory.
    This tutorial also doesn't provide any plug and play code, you'll have to do a lot of work yourself.
    There are a couple of trade-offs, some nastier, some not so much, for the security. You'll lose convenience and cross-platformingness. But that's always like this. Either you make something secure or simple. The choice is yours.
    I am not responsible for any damage you may cause to your projects or resources and neither am I for any compatibility issues you cause to your project. Backup your project before starting! What I'm doing in this tutorial is no rocket science, but you can damage your project if you do something wrong!
    Also, whatever we do here will not protect your game from hacker attacks. It's only meant to safeguard your resources. After all, the biggest security issue, the html file itself, is hardly possible to protect.

    So let's get to it.
    I can't be talking too much about how default MV encryption works, because i'd be breaking the EULA. However, there is one field of security I am allowed to talk about, because it's open source and everybody can see it freely.
    MV's savefiles.
    Let's take a look at how they are made:
    Code:
    StorageManager.saveToLocalFile = function(savefileId, json) {
        var data = LZString.compressToBase64(json);
        var fs = require('fs');
        var dirPath = this.localFileDirectoryPath();
        var filePath = this.localFilePath(savefileId);
        if (!fs.existsSync(dirPath)) {
            fs.mkdirSync(dirPath);
        }
        fs.writeFileSync(filePath, data);
    };
    
    StorageManager.loadFromLocalFile = function(savefileId) {
        var data = null;
        var fs = require('fs');
        var filePath = this.localFilePath(savefileId);
        if (fs.existsSync(filePath)) {
            data = fs.readFileSync(filePath, { encoding: 'utf8' });
        }
        return LZString.decompressFromBase64(data);
    };
    The first function is responsible for saving and the second one for loading. We can see that the file gets saved via nw.js's module "fs".
    Something we will be using to save our files once we're done with them. Other than that there's also one interesting line, the LZString method.
    LZString is a Javascript that allows us to compress a string to strings of different byte bases. In fact "compression" probably isn't the right word for it, because a base64 string is 1.3x bigger than the uncompressed version. Nevertheless, it's very popular for savefiles, although it's only good as a first layer of security, because you just decompress it and it's decrypted. Which is harmless for our JSON files, but useless for images and music. So let's take a look at some real encryption.

    Choice of encryption software
    There are multiple scripts out there that deal with encryption. Each one has unique algorithms and unique syntax, so choose whichever one you like. Whether it's encryption algorithm or hashing algorithm, it's up to you, because the decryption is always similar.
    I'll be, for this illustration, using an AES algorithm from series of Crypto.js scripts. This illustration is NOT plug and play though, because 1. You should apply your own means of protection (feel free to use the same algorithm, just don't use the same key! :D)
    2. There are some things that need to be handled first.

    Encryption of our assets
    It would be very nice if we could just mash a file into the encryption function and it would handle everything. Unfortunately, that's not how it works. Encryption algorithms handle only text strings and images and audios are binary files.
    So we need to convert an image to a dataURL before we can encrypt it. I have found a function here (credits: Guest271314) and slightly adapted it to make sure I could then manipulate the result. Let's illustrate it on Shadow1.png inside System folder, because it's a very small image:

    Code:
    var xhr = new XMLHttpRequest();  
        xhr.open("GET", "img/system/Shadow1.png", true);
        xhr.responseType = "blob";
        xhr.onload = function (e) {
                var reader = new FileReader();
                reader.onload = function(event) {
                img = event.target.result;
                }
                var file = this.response;
                reader.readAsDataURL(file)
        };
        xhr.send()
    It's very important that the img is a global variable, because if it's only a local variable, we won't be able to manipulate it. It's also important that the responseType is a blob, because readAsDataURL supports only blobs.
    Now that we have img or music converted to a string, we can now safely encrypt it. An encryption is actually a pretty easy process.
    In my case it's going to look like this:

    Code:
    var cryptoJS = require('crypto-js');
    var ciphertext = CryptoJS.AES.encrypt(img, 'secret key 123');
    var secondlayer = LZString.compressToBase64(ciphertext);
    As you can see, I only added two layers of security, first I encrypted the string and then I compressed it using LZString.
    And all that's left is to save it. We'll be using writeFileSync for it.
    Code:
    var fs = require('fs');
    fs.writeFileSync("img/System/Shadow.coolFormat", secondlayer);
    It doesn't matter which file format you want to use. You can use a completely custom format, you can overwrite the png, you can also save it as a jpeg to try and confuse potential hackers... It's completely up to you.
    However, encryption is the easier part. Decryption will be worse.

    Decryption of our assets and their use
    Decryption of the image is not a problem. We'll just decrypt it with the layers of security swapped. So since I did the compression last, I need to first decompress it before I can decrypt it.
    What I am doing here is merely following orders from the encryption script documentation, so there's nothing to explain here.

    Code:
    var fs = require('fs');
    var CryptoJS = require("crypto-js");
    
    var toDecompress = fs.readFileSync("img/System/Shadow.coolFormat", { encoding: 'utf8' });
    var decompressed = LZString.decompressFromBase64(toDecompress);
    var decrypted = CryptoJS.AES.decrypt(decompressed.toString(), 'secret key 123');
    finalImg = decrypted.toString(CryptoJS.enc.Utf8);
    finalImg can be used to freely create textures and sprites.
    Code:
    img = PIXI.Sprite.fromImage (finalImg);
    SceneManager._scene.addChild (img);
    If you aren't too familiar with PIXI, you can use default MV sprite, although you first have to generate texture for it:
    Code:
    texture = PIXI.Texture.fromImage (finalImg);
    sprite = new Sprite(texture.baseTexture);
    SceneManager._scene.addChild(sprite)
    As you can see, the encryption brings forth a new way of loading resources, requiring some edit to resource handling. But that's not all... Because while this is completely enough to both decrypt and deal with images, it's enough to only decrypt music files! WebAudio cannot use base64 files, because it can't find node points and other things it needs to find.

    So you need to convert them once again... To a blob and transform it to URL. Credit to Metal03326 for following code:
    Code:
    function dataURItoBlob(dataURI) {
            dataURI = dataURI.split( ',' );
            var type = dataURI[ 0 ].split( ':' )[ 1 ].split( ';' )[ 0 ];
            var byteString = atob( dataURI[ 1 ] );
            var byteStringLen = byteString.length;
            var ab = new ArrayBuffer( byteStringLen );
            var intArray = new Uint8Array( ab );
            for ( var i = 0; i < byteStringLen; i++ )
            {
                intArray[ i ] = byteString.charCodeAt( i );
            }
            return new Blob( [ intArray ], {type: type} );
        }
    This is of course only a function to create the blob, which unfortunately is not enough to use the audio, because there has to be an url to the source. So we need to generate URL from this blob. Eventually when we don't need it, we'll also need to get rid of it (see the post linked above for more information).

    Code:
    var fs = require('fs');
    var audio = fs.readFileSync ("audio/bgm/Castle1.coolFormat"), {encoding: 'utf8'});
    audioURL = URL.createObjectURL(dataURItoBlob(audio))
    
    Unfortunately this advance has broken all default MV use for me. So I had to use an external plugin (in this case PIXI.sound) to be able to play the music.
    Code:
    snd = PIXI.sound.Sound.from(audioURL);
    snd.loop = true;
    snd.play();
    However, I think it's worth it to invest some work in more advanced security. Although all this would be completely worthless, because no matter how strong the encryption is, JS files need to be accessible and therefore it's very easy to decrypt the files!
    But that's why there's the last step.

    JavaScript compilation
    That's right. The last step is compilation of our JS using NW.JS sdk, in particular nwjc. And it is a step that should ONLY be done to DEPLOYED projects!
    RPG maker MV supports the use of SDK by default (after all the SDK allows you to open the developer tools via f8), so you don't have to download anything.
    In order to compile your JS files, I suggest you find them, copy them and paste them inside nwjs-win-test folder inside MV. Windows Steam users should find that folder at c:\program files\Steam\steamapps\common\RPG maker MV\nwjs-win-test by default. Once you've got them there, open your command window (or shell for linux) and get in the folder. Then simply type in nwjc file finishedFilename.
    So to convert rpg_core.js type simply
    Code:
    nwjc rpg_core.js rpg_core.bin
    The format should be irrelevant.

    And last but not least, copy it back in the js folder.
    Then open your index.html. You need to tell the game to listen to your new files. Luckily it's not too hard. Open your index.html and replace
    Code:
    <script type="text/javascript" src="js/rpg_core.js"></script>
    with
    Code:
    <script>nw.Window.get().evalNWBin(null, "www/js/rpg_core.bin");</script>
    evalNWBin takes two parameters, first is frame, then comes filepath. Since we don't have any iframes and stuff, we'll leave it at null for the main frame.
    The www in the filepath is very important. Script src looks from the perspective of the index.html, but evalNWBin looks from the perspective of the executable file! (that's why you should do this only to deployed projects).

    So essentially your index.html, if you convert all core scripts, should look like this:
    Code:
    <script>nw.Window.get().evalNWBin(null, "www/js/libs/pixi.bin");</script>
    <script>nw.Window.get().evalNWBin(null, "www/js/libs/pixi-tilemap.bin");</script>
    <script>nw.Window.get().evalNWBin(null, "www/js/libs/pixi-picture.bin");</script>
    <script>nw.Window.get().evalNWBin(null, "www/js/libs/fpsmeter.bin");</script>
    .
    .
    .
    <script>nw.Window.get().evalNWBin(null, "www/js/rpg_core.bin");</script>
    <script>nw.Window.get().evalNWBin(null, "www/js/rpg_managers.bin");</script>
    .
    .
    and so on

    Why isn't this possible with earlier versions of MV
    Well... Truth is, it is possible. You just need to update nw.js to at least version 0.22 though, because versions prior to this one suffered huge performance drops as a result of compilation. It was supposedly fixed at 0.22.
    MV 1.6.0 uses NW.js 0.25.4, so it's safe from everything.

    A couple of notes
    The compiled code isn't compatible neither cross-platform nor cross-version. That means for each platform you want to deploy it to, you'll have to compile it with appropriate SDK version and system type. And I'm not entirely sure how it's handled in mobile development.
    If you want to convert your plugins as well, make sure to work around the plugins.js first, because this plugin searches for .js stuff. Also, if you want to convert plugins, you may want to include them either inside index.html or somewhere else.
    Do NOT encrypt javascripts before or after compilation. It's pointless to do it after compilation and stupid to do before compilation, because it's compilation into binary native code.
    This doesn't fully safeguard your game from hackers. Native code still has some information that can tell something and index.html is always going to be vulnerable for code injections. But at least the resources are now more secure and harder to grasp.

    A couple of other tips
    Explore, explore and explore. That's the only way to learn. Who knows, maybe you'll find better ways than I have listed here. For example the URLs created from blobs are a pretty decent protection of music resources in online games, because the blob will never point to the native file. Using that you can make a pretty safe browser game, which can prevent people from editing your index.html.
    Obfuscate your project if you want additional layer of security. Join some scripts together. Create misleading names and file formats. Create fake code that is completely worthless, but doesn't crash your game.
    Since they are text strings now, you can also join several image files together and use split method to separate them. I wouldn't try it with audio files, since they are huge.
    Don't try to open encrypted music files in a text editor. It's so long it may freeze your machine.
    Instead of readFileSync you may want to use only readFile. The difference is, readFile is an asynchronous function and will require a callback once the file is loaded, while readFileSync doesn't require callback, but freezes whole processing while the resource is loading (which is what we know as lags). And decryption and stuff is going to take more time than pure loading, so you may want to have something like asynchronous precaching system.


    Epilogue
    I know the trade-offs for additional security are huge. It involves a lot of work for who knows which results and MV loses cross platformingness, something it relies on. So I completely understand if nobody decides to build on this. However, I hope it helps somebody.
    If you have any questions, I'll try my best to answer them.
     
    Last edited: Apr 9, 2018
    #1
    boomy, DangDut, Lonewulf123 and 3 others like this.
  2. Lonewulf123

    Lonewulf123 Veteran Veteran

    Messages:
    271
    Likes Received:
    78
    Location:
    United States
    First Language:
    Enlish
    Thanks for the write up here! It's definitely advanced, but as you have stated the benefits definitely are apparent.

    I'll try to apply these on my next project!
     
    #2
    Poryg likes this.
  3. AceOfAces_Mod

    AceOfAces_Mod Engineering to infinity! Veteran

    Messages:
    1,689
    Likes Received:
    1,132
    First Language:
    Greek
    Primarily Uses:
    RMVXA
    I have a question about the Javascript Compiler part. Do I have to include all the js files I compile to the HTML file? Including the plugins?
     
    Last edited: May 9, 2018
    #3
  4. Poryg

    Poryg Pixie of the Emvee kingdom, Ham of a Hamster Veteran

    Messages:
    3,528
    Likes Received:
    8,677
    Location:
    Czech Republic
    First Language:
    Czech
    Primarily Uses:
    RMMV
    Maybe you can do it also via Javascript's function.

    In MV scripts are by default solved in such a way that the core scripts are handled via index.html and the rest are added there via document.createElement. If you edit this function, maybe you won't need to manually add them to your index file. Maybe you can also execute it from a normal encrypted file and it will be enough.
    And in case you do need to add them all manually (which I doubt though), you can always automate it with clever javascripting.
    Code:
    let fs = require("fs");
    let htmlSplit = fs.readFileSync("index.html").split("\n");
    let html = "";
    while(true) {
     if (htmlSplit[0].includes ("/body") break;
     html += htmlSplit[0];
     htmlSplit.shift();
    };
    
    let dir = fs.readdir();
    for (let i in dir) {
      html += "<script>nw.Window.get().evalNWBin('js/plugins/'" + dir[i] + ") \n";
    };
    
    while(true) {
     if (!htmlSplit.length()) break;
     html += htmlSplit[0];
     htmlSplit.shift();
    }
    
    fs.writeFileSync("index.html");
    Note that this is a code you would use in console... And only one time code. This code will overwrite your index.html, adding all plugjns instead of having to manually add them yourself. This requires to delete the code that adds new plugins via document.createElement.

    You can also invest time to edit scripts so that all scripts can without problem go inside one huge file.
    If I had my laptop, I would be able to be more precise, but currently it is dead.
     
    #4
  5. Poryg

    Poryg Pixie of the Emvee kingdom, Ham of a Hamster Veteran

    Messages:
    3,528
    Likes Received:
    8,677
    Location:
    Czech Republic
    First Language:
    Czech
    Primarily Uses:
    RMMV
    Time has passed and with it an update is needed, because, among other things, I have gained crucial experience in the matter of resource protection, which has shown a critical weakness in what I have shown here.

    Among other things though. Before I start, I first need to mention that @AceOfAces_Mod has released a cook tool for MV, so dealing with the javascript compilation should be much simpler.
    Nevertheless, it's pretty much the only thing that, as it is, can be considered safe.

    The thing is, no matter how you encrypt the resources, they always need to be decrypted into something a computer can understand.
    So in the end if the perpetrator can get in the dev tools, they can steal the resources quite easily. I will not describe how, since it would be supporting illegal activities.
    So if you want to make it more difficult, you need to prevent their access to the dev tools. Which is impossible in case of browser games, since every browser has dev tools built in.
    So essentially if you release a normal online game, using MV's default encryption is enough, because you will not be able to make it any safer regardless of what you do.

    Nevertheless, it's not impossible for Nwjs. And huge thanks to @Isabella Ava for her challenge, which gave me the last thing I needed to know before writing this update post.

    I guess many people already know about Enigma virtual box, because it allows us to distribute our game in a single file. Someone may use it as a security tool (or they just stuff every one of their game files), but that is not a good idea.
    Enigma virtual box is not a security tool and was never meant to be. It's only a wrapper, which will allow you to wrap X files together into one huge file. And it's very easy to unpack these files.
    Also, wrappers and obfuscators result in decreased performance. If you're reasonable, it's not too drastic, but the fact still stands that it's a memory hog.

    Nevertheless, using a wrapper is pretty much a necessity. After all, these are all files from which dev tools can be opened on a deployed project with no nasty shenanigans done to them (i.e. with no editing other than injecting the script code):
    • index.html
    • any js files
    • package.json
    • any map.json files
    • Common events.json
    • any compiled js files that use eval
    • depending on plugins, also other json files (for example Yanfly lunatic mode allows the execution of javascript code, so through that it's possible)
    and something has to be done with them. If you take a look at it though, you'll see that the only files that actually need to be wrapped are js files, their compiled versions if you want to be extra safe, and the json files. No code can be executed from png images or audio, so we can keep them unwrapped, greatly reducing the memory hog - as long as you don't use default MV encryption or some other forms of simple encryption, they should be ok unwrapped.

    So, which wrapper would you want to use?
    Definitely not one which has an unpacker online for download. It can also help if the wrapper also has features like Anti-debugger for additional protection. In Enigma's case it would be Enigma protector.
    Whatever you use though, remember that it is entirely impossible to 100% prevent your resources from being ripped. The purpose of all this is to just decrease the chance of it happening. After all, the chance of somebody wanting to rip resources from your game can be pretty high. But what's the chance of that person being a skilled hacker or someone being well versed in low level languages, since we're talking about an indie rpg maker game? Not so great anymore.
     
    #5

Share This Page