/** * Author: Peter Dragicevic [peter@petschko.org] * Authors-Website: http://petschko.org/ * Date: 30.03.2017 * Time: 23:05 */ /** * Creates a new instance of the Decrypter Object * * @param {string|null} encryptionKey - Encryption-Key * @constructor - Decrypter */ function Decrypter(encryptionKey) { // Encryption-Fields this.encryptCode = encryptionKey; // Option Fields this.ignoreFakeHeader = false; // Fake-Header Info-Fields this.headerLen = null; this.signature = null; this.version = null; this.remain = null; // Setup how long the PNG-Header is this.pngHeaderLen = null; /** * Splits the Encryption-Code into an Array * * @returns {Array} - Encryption-Array */ Decrypter.prototype.splitEncryptionCode = function() { if(! this.encryptCode) return []; return this.encryptCode.split(/(.{2})/).filter(Boolean); }; this.encryptionCodeArray = this.splitEncryptionCode(); /** * Check if the current File-Header matches the Fake-Header * * @param {Uint8Array} fileHeader - Current-File-Header * @returns {boolean} - true if header matches fake header else false */ Decrypter.prototype.verifyFakeHeader = function(fileHeader) { var fakeHeader = this.buildFakeHeader(); for(var i = 0; i < this.getHeaderLen(); i++) if(fileHeader[i] !== fakeHeader[i]) return false; return true; }; /** * Builds the Fake-Header * * @returns {Uint8Array} - Fake-Header-Array */ Decrypter.prototype.buildFakeHeader = function() { var fakeHeader = new Uint8Array(this.getHeaderLen()); var headerStructure = this.getSignature() + this.getVersion() + this.getRemain(); for(var i = 0; i < this.getHeaderLen(); i++) fakeHeader[i] = parseInt('0x' + headerStructure.substr(i * 2, 2), 16); return fakeHeader; }; /** * Do something with a RPGFile * * @param {RPGFile} rpgFile - RPGFile Object * @param {string} modType - Specify what to do with that file * @param {function} callback - Function if operation is done */ Decrypter.prototype.modifyFile = function(rpgFile, modType, callback) { var reader = new FileReader(); var that = this; reader[window.addEventListener ? 'addEventListener' : 'attachEvent'] (window.addEventListener ? 'load' : 'onload', function() { console.log('Try to ' + modType + ' the File "' + rpgFile.name + '.' + rpgFile.extension + '...'); switch(modType) { case 'restore': try { rpgFile.content = that.restorePngHeader(this.result); rpgFile.createBlobUrl(true); } catch(e) { callback(rpgFile, e); return; } break; case 'encrypt': try { rpgFile.content = that.encrypt(this.result); rpgFile.createBlobUrl(false) } catch(e) { callback(rpgFile, e); return; } break; case 'decrypt': default: try { rpgFile.content = that.decrypt(this.result); rpgFile.createBlobUrl(true); } catch(e) { callback(rpgFile, e); return; } } callback(rpgFile, null); }, false); reader.readAsArrayBuffer(rpgFile.file); }; /** * Restores the header of an encrypted PNG-File without the key * * @param {ArrayBuffer} arrayBuffer - Array-Buffer of the File * @returns {ArrayBuffer} - Array-Buffer of the restored File */ Decrypter.prototype.restorePngHeader = function(arrayBuffer) { if(! arrayBuffer) throw new ErrorException(html(lang.strings['exception.emptyFile']), 1); var headerLen = (this.pngHeaderLen === null) ? this.getHeaderLen() : this.pngHeaderLen; var pngStartHeader = Decrypter.getNormalPNGHeader(headerLen); // Make sure that to long header values get the correct one headerLen = pngStartHeader.length; // Remove Fake-Header and encrypted Bytes arrayBuffer = arrayBuffer.slice(headerLen * 2, arrayBuffer.byteLength); // Merge the file-content with the header var tmpInt8Arr = new Uint8Array(arrayBuffer.byteLength + headerLen); tmpInt8Arr.set(pngStartHeader, 0); tmpInt8Arr.set(new Uint8Array(arrayBuffer), headerLen); return tmpInt8Arr.buffer; }; /** * Decrypts a RPG-Make-File-ArrayBuffer & may check the header if turned on * * @param {ArrayBuffer} arrayBuffer - Array-Buffer of the File * @returns {ArrayBuffer} - Decrypted Array-Buffer of the File without the Fake-Header */ Decrypter.prototype.decrypt = function(arrayBuffer) { if(! arrayBuffer) throw new ErrorException(html(lang.strings['exception.emptyFile']), 1); if(! this.ignoreFakeHeader) { var header = new Uint8Array(arrayBuffer, 0, this.getHeaderLen()); if(! this.verifyFakeHeader(header)) throw new ErrorException( html(lang.strings['exception.invalidFakeHeader.1']) + ' ' + html(lang.strings['exception.invalidFakeHeader.2']), 2 ); } // Remove the Fake-Header from File-arrayBuffer arrayBuffer = arrayBuffer.slice(this.getHeaderLen(), arrayBuffer.byteLength); // Decrypt File beginning arrayBuffer = this.xOrBytes(arrayBuffer); return arrayBuffer; }; /** * (Re)-Encrypt a RPG-Make-File-ArrayBuffer * * @param {ArrayBuffer} arrayBuffer - Array-Buffer of the File * @returns {ArrayBuffer} - Encrypted Array-Buffer with the Fake-Header */ Decrypter.prototype.encrypt = function(arrayBuffer) { if(! arrayBuffer) throw new ErrorException(html(lang.strings['exception.emptyFile']), 1); // Encrypt the File beginning arrayBuffer = this.xOrBytes(arrayBuffer); // Create Header var fakeHeader = this.buildFakeHeader(); // Add Fake-Header in Front then the File var tmpInt8Array = new Uint8Array(arrayBuffer.byteLength + this.getHeaderLen()); tmpInt8Array.set(fakeHeader, 0); tmpInt8Array.set(new Uint8Array(arrayBuffer), this.getHeaderLen()); // Check if header is valid var header = new Uint8Array(tmpInt8Array.buffer, 0, this.getHeaderLen()); if(! this.verifyFakeHeader(header)) throw new ErrorException( html(lang.strings['exception.invalidFakeHeader.1']) + ' ' + html(lang.strings['exception.reportBug']), 3 ); return tmpInt8Array.buffer; }; /** * XOR x Bytes (x = header-length-bytes) * * @param {ArrayBuffer} arrayBuffer - Array-Buffer where bytes should be XORed * @returns {ArrayBuffer} - Array-Buffer with XORed Bytes */ Decrypter.prototype.xOrBytes = function(arrayBuffer) { var view = new DataView(arrayBuffer); if(arrayBuffer) { var byteArray = new Uint8Array(arrayBuffer); for(var i = 0; i < this.getHeaderLen(); i++) { byteArray[i] = byteArray[i] ^ parseInt(this.encryptionCodeArray[i], 16); view.setUint8(i, byteArray[i]); } } return arrayBuffer; } } // Class constants Decrypter.prototype.defaultHeaderLen = 16; Decrypter.prototype.defaultSignature = "5250474d56000000"; Decrypter.prototype.defaultVersion = "000301"; Decrypter.prototype.defaultRemain = "0000000000"; Decrypter.pngHeaderBytes = '89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52'; /** * Returns the Header Len * * @returns {int} - Header-Len */ Decrypter.prototype.getHeaderLen = function() { if(this.headerLen === null || typeof this.headerLen !== 'number') this.headerLen = this.defaultHeaderLen; // Ensure int return Math.floor(this.headerLen); }; /** * Returns the Signature * * @returns {string} - Signature */ Decrypter.prototype.getSignature = function() { if(this.signature === null) this.signature = this.defaultSignature; return this.signature; }; /** * Returns the Version * * @returns {string} - Version */ Decrypter.prototype.getVersion = function() { if(this.version === null) this.version = this.defaultVersion; return this.version; }; /** * Returns the Remain * * @returns {string} - Remain */ Decrypter.prototype.getRemain = function() { if(this.remain === null) this.remain = this.defaultRemain; return this.remain; }; /** * Restores a RPGMVP-File without key * * @param {RPGFile} rpgFile - RPGFile to Decrypt * @param {function} callback - Function if operation is done */ Decrypter.prototype.restoreHeader = function(rpgFile, callback) { this.modifyFile(rpgFile, 'restore', callback); }; /** * Decrypts a RPGFile * * @param {RPGFile} rpgFile - RPGFile to Decrypt * @param {function} callback - Function if operation is done */ Decrypter.prototype.decryptFile = function(rpgFile, callback) { this.modifyFile(rpgFile, 'decrypt', callback); }; /** * Encrypts a RPGFile * * @param {RPGFile} rpgFile - RPGFile to Decrypt * @param {function} callback - Function if operation is done */ Decrypter.prototype.encryptFile = function(rpgFile, callback) { this.modifyFile(rpgFile, 'encrypt', callback) }; /** * Get the normal starting PNG-Header * * @param {int} headerLen - Header Length * @returns {Uint8Array} - Original starting PNG-Header */ Decrypter.getNormalPNGHeader = function(headerLen) { var headerToRestore = Decrypter.pngHeaderBytes.split(' '); // Header can't be longer than restore string if(headerLen > headerToRestore.length) headerLen = headerToRestore.length; var restoredHeader = new Uint8Array(headerLen); for(var i = 0; i < headerLen; i++) restoredHeader[i] = parseInt('0x' + headerToRestore[i], 16); return restoredHeader; }; /** * Detect the Encryption-Code from a RPGFile * * @param {RPGFile} rpgFile - RPGFile Object * @param {int} headerLen - Header-Length * @param {function} callback - Function if operation is done */ Decrypter.detectEncryptionCode = function(rpgFile, headerLen, callback) { var reader = new FileReader(); reader[window.addEventListener ? 'addEventListener' : 'attachEvent'] (window.addEventListener ? 'load' : 'onload', function() { var key; var fileContent; // Try to get the key from Image if(rpgFile.isEncryptedImg()) { key = Decrypter.getKeyFromPNG(headerLen, this.result); // todo replace header len if(key !== null) { callback(key); return; } } var fileContentAsText = new TextDecoder().decode(new Uint8Array(this.result)); try { fileContent = JSON.parse('[' + fileContentAsText + ']'); key = fileContent[0].encryptionKey; } catch(e) { // Try if it is LZ-String compressed var lzUncompressed = LZString.decompressFromBase64(fileContentAsText); try { fileContent = JSON.parse('[' + lzUncompressed + ']'); key = fileContent[0].encryptionKey; } catch(e) { // Still no JSON-File... key = null; } } // Try a search if(key === null) key = Decrypter.searchEncryptionCode(fileContentAsText, 'rpg_core', false); callback(key); }, false); reader.readAsArrayBuffer(rpgFile.file); }; /** * Gets the Key from PNG File * * @param {int} headerLen - Header Length * @param {ArrayBuffer} fileContent - File-Content which should be tried to get the key from * @return {string|null} - Key or null of not found/valid */ Decrypter.getKeyFromPNG = function(headerLen, fileContent) { if(! fileContent) return null; if(fileContent.byteLength < headerLen * 2) return null; var fileHeader = fileContent.slice(headerLen, headerLen * 2); var fileHeaderU8 = new Uint8Array(fileHeader); var maybeKeyBytes = new Uint8Array(headerLen); var pngHeaderU8 = Decrypter.getNormalPNGHeader(headerLen); // Get the potential Key var i; var tmpByte; var output = ''; for(i = 0; i < headerLen; i++) { maybeKeyBytes[i] = pngHeaderU8[i] ^ fileHeaderU8[i]; output += '' + Decrypter.byteToHex(maybeKeyBytes[i]); } return output; }; /** * Searches for the encryption-key in other places * * @param {string} fileContent - Content of the File, where to search * @param {string} searchParam - What method should be used to search * @param {boolean} lzString - Decompress LZ-String * @returns {null|string} - null if the key was not found else the key */ Decrypter.searchEncryptionCode = function(fileContent, searchParam, lzString) { var result = null; fileContent = (lzString) ? LZString.decompressFromBase64(fileContent) : fileContent; // Exit on empty File-Content (Usually caused if LZ-String-Decompress was not an LZ-String) if(! fileContent) return null; switch(searchParam) { case 'rpg_core': var innerFunctionCodeMatches = null; var lines = fileContent.split('\n'); for(var line = 0; line < lines.length; line++) { var l = lines[line]; // Clean the line l = l.trim(); l = l.replace(/[\r\n\t]/g, ''); innerFunctionCodeMatches = l.match(/^(.*)this\._encryptionKey ?= ?"(.*)"(.*);(.*)?$/); if(innerFunctionCodeMatches !== null) break; } if(innerFunctionCodeMatches && innerFunctionCodeMatches.length > 2) result = innerFunctionCodeMatches[2]; // Verify result if(! result || typeof result === 'undefined') result = null; // Also try a LZ-String search if(result === null && ! lzString) result = Decrypter.searchEncryptionCode(fileContent, searchParam, true); return result; default: return null; } }; /** * Converts a Byte to a Hex string * * @param {int} byte - Byte Value * @returns {string} - Hex Value */ Decrypter.byteToHex = function(byte) { return ('0' + (byte & 0xFF).toString(16)).slice(-2); }; /** * Check if the string only has HEX-Chars (0-9 & A-F) * * @param {string} string - String to verify * @returns {boolean} - true if string is valid else false */ Decrypter.checkHexChars = function(string) { var regex = new RegExp(/^[A-Fa-f0-9]+$/); return regex.test(string); }; /** * Converts a Byte to Bits * * @param {number} byte - Byte * @returns {string} - Bits */ Decrypter.helperShowBits = function(byte) { if(isNaN(byte)) byte = 0; if(byte > 255 || byte < 0) throw html(lang.strings['exception.helper.invalidByte']).replace('{0}', byte.toString()); var bits = byte.toString(2); var missingZeros = 8 - bits.length; return new Array(missingZeros + 1).join('0') + bits; };