commit 50b84ae88c6bc7ebe7b2363f81c9251abbddd5c4 Author: Mason <1281814306@qq.com> Date: Tue Nov 30 12:47:53 2021 +0800 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..8eb7471 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# typora Cracker + +A patch and keygen tools for typora. + +中文说明请戳[这里](README_CN.md) + +## WARNING +``` +FOR STUDY AND DISCUSSION ONLY, PLEASE DO NOT ENGAGE IN ANY ILLEGAL ACTS. +ANY PROBLEMS ARISING FROM THIS WILL BE BORNE BY THE USER (YOU). +``` + +## Features + +- Supports ALL OS supported by typora + +## Usage + +1. `pip install -r requirements.txt` +2. `python typroa.py --help` +3. read and use. +4. patch License.js. +5. replace app.asar. +6. run keygen. +7. enjoy it. + + +## Example + +```shell +> python typroa.py --help +usage: typora.py [-h] [-u] [-f] asarPath dirPath + +extract and decryption app.asar file from [Typora]. + +positional arguments: + asarPath app.asar file path [input/ouput] + dirPath as tmp and out directory. + +optional arguments: + -h, --help show this help message and exit + -u pack & encryption (default: extract & decryption) + -f enabled prettify/compress (default: disabled + +If you have any questions, please contact [ MasonShi@88.com ] + +> python typora.py {installRoot}/Typora/resources/app.asar workstation/outfile/ +⋯ +> python typora.py -u workstation/outfile/ workstation/outappasar +⋯ +> cp {installRoot}/Typora/resources/app.asar {installRoot}/Typora/resources/app.asar.bak +> mv workstation/outappasar/app.asar {installRoot}/Typora/resources/app.asar +# (patch code) +> node keygen.js +XXXXXX-XXXXXX-XXXXXX-XXXXXX +> typora +# (input info) +email: crack@example.com +serial: XXXXXX-XXXXXX-XXXXXX-XXXXXX +``` + +## LICENSE + MIT LICENSE \ No newline at end of file diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..3bd23fa --- /dev/null +++ b/README_CN.md @@ -0,0 +1,61 @@ +# typora Cracker + +一个typora的Patch和KeyGen工具 + +## 敬告 +``` +仅供学习和讨论,请不要从事任何非法行为。 +由此产生的任何问题都将由用户(您)承担。 +``` + +## Features + +- 理论上支持Typora支持的所有操作系统 + +## 食用方式 + +1. `pip install -r requirements.txt` +2. `python typroa.py --help` +3. 阅读帮助文档及使用。 +4. 修改导出的 License.js。 +5. 替换原目录下的 app.asar。 +6. 运行KeyGen程序。 +7. 正常激活。 + + +## 示例 + +```shell +> python typroa.py --help +usage: typora.py [-h] [-u] [-f] asarPath dirPath + +extract and decryption app.asar file from [Typora]. + +positional arguments: + asarPath app.asar file path [input/ouput] + dirPath as tmp and out directory. + +optional arguments: + -h, --help show this help message and exit + -u pack & encryption (default: extract & decryption) + -f enabled prettify/compress (default: disabled + +If you have any questions, please contact [ MasonShi@88.com ] + +> python typora.py {installRoot}/Typora/resources/app.asar workstation/outfile/ +⋯ +> python typora.py -u workstation/outfile/ workstation/outappasar +⋯ +> cp {installRoot}/Typora/resources/app.asar {installRoot}/Typora/resources/app.asar.bak +> mv workstation/outappasar/app.asar {installRoot}/Typora/resources/app.asar +# (patch code) +> node keygen.js +XXXXXX-XXXXXX-XXXXXX-XXXXXX +> typora +# (input info) +email: crack@example.com +serial: XXXXXX-XXXXXX-XXXXXX-XXXXXX +``` + +## LICENSE + MIT LICENSE \ No newline at end of file diff --git a/keygen.js b/keygen.js new file mode 100644 index 0000000..5bc9850 --- /dev/null +++ b/keygen.js @@ -0,0 +1,17 @@ +function randomSerial() { + var $chars = 'L23456789ABCDEFGHJKMNPQRSTUVWXYZ'; + var maxPos = $chars.length; + var serial = ''; + for (i = 0; i < 22; i++) { + serial += $chars.charAt(Math.floor(Math.random() * maxPos)); + } + serial += (e => { + for (var t = "", i = 0; i < 2; i++) { + for (var a = 0, s = 0; s < 16; s += 2) a += $chars.indexOf(e[i + s]); + t += $chars[a %= $chars.length] + } + return t + })(serial) + return serial.slice(0, 6) + "-" + serial.slice(6, 12) + "-" + serial.slice(12, 18) + "-" + serial.slice(18, 24); +} +console.log(randomSerial()); diff --git a/masar.py b/masar.py new file mode 100644 index 0000000..65f81d5 --- /dev/null +++ b/masar.py @@ -0,0 +1,176 @@ +# -*- coding:utf-8 -*- +""" +@Author: Mas0n +@File: masar.py +@Time: 2021-11-29 22:34 +@Desc: It's all about getting better. +""" +import os +import errno +import io +import struct +import shutil +import fileinput +import json + + +def round_up(i, m): + return (i + m - 1) & ~(m - 1) + + +class Asar: + def __init__(self, path, fp, header, base_offset): + self.path = path + self.fp = fp + self.header = header + self.base_offset = base_offset + + @classmethod + def open(cls, path): + fp = open(path, 'rb') + data_size, header_size, header_object_size, header_string_size = struct.unpack('<4I', fp.read(16)) + header_json = fp.read(header_string_size).decode('utf-8') + return cls( + path=path, + fp=fp, + header=json.loads(header_json), + base_offset=round_up(16 + header_string_size, 4) + ) + + @classmethod + def compress(cls, path): + offset = 0 + paths = [] + + def _path_to_dict(path): + nonlocal offset, paths + result = {'files': {}} + for f in os.scandir(path): + if os.path.isdir(f.path): + result['files'][f.name] = _path_to_dict(f.path) + elif f.is_symlink(): + result['files'][f.name] = { + 'link': os.path.realpath(f.name) + } + # modify + elif f.name == "main.node": + size = f.stat().st_size + result['files'][f.name] = { + 'size': size, + "unpacked": True + } + else: + paths.append(f.path) + size = f.stat().st_size + result['files'][f.name] = { + 'size': size, + 'offset': str(offset) + } + offset += size + return result + + def _paths_to_bytes(paths): + _bytes = io.BytesIO() + with fileinput.FileInput(files=paths, mode="rb") as f: + for i in f: + _bytes.write(i) + return _bytes.getvalue() + + header = _path_to_dict(path) + header_json = json.dumps(header, sort_keys=True, separators=(',', ':')).encode('utf-8') + header_string_size = len(header_json) + data_size = 4 + aligned_size = round_up(header_string_size, data_size) + header_size = aligned_size + 8 + header_object_size = aligned_size + data_size + diff = aligned_size - header_string_size + header_json = header_json + b'\0' * diff if diff else header_json + fp = io.BytesIO() + fp.write(struct.pack('<4I', data_size, header_size, header_object_size, header_string_size)) + fp.write(header_json) + fp.write(_paths_to_bytes(paths)) + + return cls( + path=path, + fp=fp, + header=header, + base_offset=round_up(16 + header_string_size, 4)) + + def _copy_unpacked_file(self, source, destination): + unpacked_dir = self.path + '.unpacked' + if not os.path.isdir(unpacked_dir): + print("Couldn't copy file {}, no extracted directory".format(source)) + return + + src = os.path.join(unpacked_dir, source) + if not os.path.exists(src): + print("Couldn't copy file {}, doesn't exist".format(src)) + return + + dest = os.path.join(destination, source) + shutil.copyfile(src, dest) + + def _extract_file(self, source, info, destination): + if 'offset' not in info: + self._copy_unpacked_file(source, destination) + return + + self.fp.seek(self.base_offset + int(info['offset'])) + r = self.fp.read(int(info['size'])) + + dest = os.path.join(destination, source) + with open(dest, 'wb') as f: + f.write(r) + + def _extract_link(self, source, link, destination): + dest_filename = os.path.normpath(os.path.join(destination, source)) + link_src_path = os.path.dirname(os.path.join(destination, link)) + link_to = os.path.join(link_src_path, os.path.basename(link)) + + try: + os.symlink(link_to, dest_filename) + except OSError as e: + if e.errno == errno.EXIST: + os.unlink(dest_filename) + os.symlink(link_to, dest_filename) + else: + raise e + + def _extract_directory(self, source, files, destination): + dest = os.path.normpath(os.path.join(destination, source)) + + if not os.path.exists(dest): + os.makedirs(dest) + + for name, info in files.items(): + item_path = os.path.join(source, name) + + if 'files' in info: + self._extract_directory(item_path, info['files'], destination) + elif 'link' in info: + self._extract_link(item_path, info['link'], destination) + else: + self._extract_file(item_path, info, destination) + + def extract(self, path): + if not os.path.isdir(path): + raise NotADirectoryError() + self._extract_directory('.', self.header['files'], path) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.fp.close() + + +def pack_asar(source, dest): + with Asar.compress(source) as a: + with open(dest, 'wb') as fp: + a.fp.seek(0) + fp.write(a.fp.read()) + + +def extract_asar(source, dest): + with Asar.open(source) as a: + a.extract(dest) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..68ad2b6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +jsbeautifier==1.14.0 +jsmin==3.0.0 +loguru==0.5.3 +pycryptodome==3.11.0 diff --git a/typora.py b/typora.py new file mode 100644 index 0000000..22ed78d --- /dev/null +++ b/typora.py @@ -0,0 +1,151 @@ +# -*- coding:utf-8 -*- +""" +@Author: Mas0n +@File: typora.py +@Time: 2021-11-29 21:24 +@Desc: It's all about getting better. +""" +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad +from Crypto.Util.Padding import unpad +from base64 import b64decode, b64encode +from jsbeautifier import beautify +from jsmin import jsmin +from os import listdir, urandom, makedirs +from os.path import isfile, isdir, join as pjoin, split as psplit +from loguru import logger as log +from masar import extract_asar, pack_asar +import argparse + +key = [0x4B029A9482B3E14E, 0xF157FEB4B4522F80, 0xE25692105308F4BE, 0x6DD58DDDA3EC0DC2] +aesKey = b"" +for akey in key: + aesKey += int.to_bytes(akey, byteorder="little", length=8) + + +def _mkdir(_path): + try: + makedirs(_path) + except FileExistsError: + log.warning(f"May FolderExists: {_path}") + + +def decScript(b64: bytes, prettify: bool): + lCode = b64decode(b64) + # iv: the first 16 bytes of the file + aesIv = lCode[0:16] + # cipher text + cipherText = lCode[16:] + # AES 256 CBC + ins = AES.new(key=aesKey, iv=aesIv, mode=AES.MODE_CBC) + code = unpad(ins.decrypt(cipherText), 16, 'pkcs7') + if prettify: + code = beautify(code.decode()).encode() + return code + + +def extractWdec(asarPath, path, prettify): + """ + :param asarPath: asar out dir + :param path: out dir + :return: None + """ + # try to create empty dir to save extract files + path = pjoin(path, "tmp_app") + _mkdir(path) + log.info(f"extract asar file: {asarPath}") + # extract app.asar to {path}/* + extract_asar(asarPath, path) + log.success(f"extract ended.") + + log.info(f"read Directory: {path}") + # construct the save directory {pathRoot}/dec_app + outPath = pjoin(psplit(path)[0], "dec_app") + # try to create empty dir to save decryption files + _mkdir(outPath) + log.info(f"set Directory: {outPath}") + # enumerate extract files + fileArr = listdir(path) + for name in fileArr: + # read files content + fpath = pjoin(path, name) + scode = open(fpath, "rb").read() + log.info(f"open file: {name}") + # if file suffix is *.js then decryption file + if isfile(fpath) and name.endswith(".js"): + scode = decScript(scode, prettify) + else: + log.debug(f"skip file: {name}") + # save content {outPath}/{name} + open(pjoin(outPath, name), "wb").write(scode) + log.success(f"decrypt and save file: {name}") + + +def encScript(_code: bytes, compress): + if compress: + _code = jsmin(_code.decode(), quote_chars="'\"`").encode() + aesIv = urandom(16) + cipherText = aesIv + _code + ins = AES.new(key=aesKey, iv=aesIv, mode=AES.MODE_CBC) + enc = ins.encrypt(pad(cipherText, 16, 'pkcs7')) + lCode = b64encode(enc) + return lCode + + +def packWenc(path, outPath, compress): + """ + :param path: out dir + :param outPath: pack path app.asar + :param compress: Bool + :return: None + """ + if not isdir(outPath): + log.error("plz input Directory for app.asar") + raise NotADirectoryError + encFilePath = pjoin(psplit(outPath)[0], "enc_app") + _mkdir(encFilePath) + + outFilePath = pjoin(outPath, "app.asar") + log.info(f"set outFilePath: {outFilePath}") + fileArr = listdir(path) + + for name in fileArr: + fpath = pjoin(path, name) + if isdir(fpath): + log.error("TODO: found folder") + raise IsADirectoryError + + scode = open(fpath, "rb").read() + log.info(f"open file: {name}") + if isfile(fpath) and name.endswith(".js"): + scode = encScript(scode, compress) + + open(pjoin(encFilePath, name), "wb").write(scode) + log.success(f"encrypt and save file: {name}") + + log.info("ready to pack") + pack_asar(encFilePath, outFilePath) + log.success("pack done") + + +def main(): + argParser = argparse.ArgumentParser( + description="[extract and decryption / pack and encryption] app.asar file from [Typora].", + epilog="If you have any questions, please contact [ MasonShi@88.com ]") + argParser.add_argument("asarPath", type=str, help="app.asar file path/dir [input/ouput]") + argParser.add_argument("dirPath", type=str, help="as tmp and out directory.") + + argParser.add_argument('-u', dest='mode', action='store_const', + const=packWenc, default=extractWdec, + help='pack & encryption (default: extract & decryption)') + argParser.add_argument('-f', dest='format', action='store_const', + const=True, default=False, + help='enabled prettify/compress (default: disabled)') + args = argParser.parse_args() + + args.mode(args.asarPath, args.dirPath, args.format) + log.success("Done!") + + +if __name__ == '__main__': + main()