ti_recover
Version:
Appcelerator Titanium APK source code recovery tool
181 lines (171 loc) • 6.41 kB
JavaScript
/*
reads AssetCryptImpl files and return struct with Titanium file names and source codes.
*/
var fs = require('fs'),
path = require('path'),
lineReader = require('line-reader'),
java = require('java');
java.classpath.push(__dirname+path.sep+'java/commons-lang-2.6.jar');
java.options.push('-Xrs'); // reduce signal os signals
var resp = {};
var meta = {
totalBytes : 0,
ti_version : -1,
alloy : false
};
var classes = {
charset : java.import('java.nio.charset.Charset'),
integer : java.import('java.lang.Integer'),
string : java.import('java.lang.String'),
escapeu : java.import('org.apache.commons.lang.StringEscapeUtils'),
charbuf : java.import('java.nio.CharBuffer'),
_javalog : java.import('java.util.logging.LogManager')
};
var _config = {
smali : '',
java : '',
apk : '',
debug : true
};
var init = function(config, onReady) {
// config
for (var _c in config) _config[_c] = config[_c];
// turn standard java logger off
classes._javalog.getLogManagerSync().resetSync();
// ready
onReady();
};
var decrypt = function(onReady) {
// read AssetCryptImpl.smali (for ranges)
// get bytes from smali
var bytesC = { start:false, bufferlen:0, charbuf:'', line:'', array:[] };
var count = 0;
if (_config.smali!='' && _config.java!='') {
lineReader.eachLine(_config.smali, function(line, last){
bytesC.line = line;
if (line.indexOf('private static initAssetsBytes()Ljava/nio/CharBuffer')!=-1) {
bytesC.start = true;
} else if (bytesC.start && line.indexOf('const v0, ')!=-1) {
// titanium < v5
meta.ti_version = -5;
bytesC.line = line.split('const v0, ').join('').trim();
bytesC.bufferlen = classes.integer.decodeSync(bytesC.line);
bytesC.charbuf = classes.charbuf.allocateSync(bytesC.bufferlen);
} else if (bytesC.start && line.indexOf('const/16 v0, ')!=-1) {
// titanium v5.x +
meta.ti_version = 5;
bytesC.line = line.split('const/16 v0, ').join('').trim();
bytesC.bufferlen = classes.integer.decodeSync(bytesC.line);
bytesC.charbuf = classes.charbuf.allocateSync(bytesC.bufferlen);
} else if (bytesC.start && line.indexOf('const-string v1')!=-1) {
// content
bytesC.line = line.split('const-string v1, "').join('').trim();
bytesC.line = bytesC.line.slice(0,-1); // remove last " char
bytesC.line = classes.escapeu.unescapeJavaSync(bytesC.line);
bytesC.charbuf.append(bytesC.line);
} else if (line.indexOf('rewind()Ljava/nio/Buffer;')!=-1) {
bytesC.charbuf.rewind();
if (_config.debug) console.log('decoding bytes ...');
/* */
bytesC.assetBytes = classes.charset.forNameSync('ISO-8859-1').encodeSync(bytesC.charbuf).arraySync();
if (_config.debug) console.log('converting into java array of bytes ... takes some time');
var iii, _cnt=0, _inbytes = [];
for (iii in bytesC.assetBytes) {
_inbytes.push(java.newByte(bytesC.assetBytes[iii]));
}
_inbytes2 = java.newArray("byte",_inbytes);
//read file byte ranges from AssetCryptImpl.java
var passed_maps = false;
if (_config.debug) console.log('extracting file ranges ...');
meta.totalBytes = 0;
lineReader.eachLine(_config.java, function(line2, last2) {
var tmp = {};
if (line2.indexOf('hashMap.put')!=-1) {
tmp.file = line2.split(',')[0].split('hashMap.put(').join('').split('"').join('').trim();
tmp.offset = line2.split(',')[1].split('new Range(').join('').trim();
tmp.length = line2.split(',')[2].split('));').join('').trim();
try {
resp[tmp.file] = {
offset : classes.integer.decodeSync(tmp.offset),
bytes : classes.integer.decodeSync(tmp.length)
};
resp[tmp.file].content = _filterDataInRange(tmp.file, _inbytes2, resp[tmp.file].offset, resp[tmp.file].bytes);
meta.totalBytes += resp[tmp.file].bytes;
} catch(eeee) {
}
passed_maps = true;
} else {
if (passed_maps) {
onReady(false, resp);
return false;
}
}
});
}
});
} else {
onReady(true,{});
}
};
var _filterDataInRange = function(filename, ibytes, offset, length) {
var _resp = '', _respb = '', _bytes_len=ibytes.length;
var key = java.import('javax.crypto.spec.SecretKeySpec');
// FIRST ATTEMPT
try {
// titanium below 3.2.2 and 3.4.0 decryption requires byteslen - 1
_bytes_len = ibytes.length-1;
var secretKeySpec = new key( ibytes,
_bytes_len - classes.integer.decodeSync("0x10"),
classes.integer.decodeSync("0x10"),
'AES');
var _cipher = java.import('javax.crypto.Cipher').getInstanceSync('AES');
var _decrypt_mode = 2; //cipher["DECRYPT_MODE"];
_cipher.initSync(_decrypt_mode, secretKeySpec);
try {
_respb = _cipher.doFinalSync(ibytes, offset, length);
_resp = String.fromCharCode.apply(null, new Uint16Array(_respb));
} catch(e1a) {
_respb = _cipher.doFinalSync(ibytes, offset-1, length); //some files have the offset padded
_resp = String.fromCharCode.apply(null, new Uint16Array(_respb));
}
} catch(e1) {
_resp = '';
}
// SECOND ATTEMPT
if (_resp=='') {
try {
// titanium over v3.4.0
_bytes_len = ibytes.length;
var secretKeySpec = new key( ibytes,
_bytes_len - classes.integer.decodeSync("0x10"),
classes.integer.decodeSync("0x10"),
'AES');
var _cipher = java.import('javax.crypto.Cipher').getInstanceSync('AES');
var _decrypt_mode = 2; //cipher["DECRYPT_MODE"];
_cipher.initSync(_decrypt_mode, secretKeySpec);
try {
_respb = _cipher.doFinalSync(ibytes, offset, length);
_resp = String.fromCharCode.apply(null, new Uint16Array(_respb));
} catch(e1a) {
_respb = _cipher.doFinalSync(ibytes, offset-1, length); //some files have the offset padded
_resp = String.fromCharCode.apply(null, new Uint16Array(_respb));
}
} catch(e2) {
_resp = '';
}
}
if (_resp!='' && _config.debug) console.log('file:'+filename+', decrypted !');
return _resp;
};
var extract = function(outputdir, onReady) {
// writes the decrypted files from memory (var resp) into the given directory (creating as needed)
};
exports.init = init;
exports.decrypt = decrypt;
exports.extract = extract;
/* uncomment for testing
init({ smali:'test/AssetCryptImpl.smali', java:'test/AssetCryptImpl.java' }, function() {
decrypt(function(err, full) {
if (!err) console.log(full);
});
});*/