Initial commit

This commit is contained in:
Mason
2021-11-30 12:47:53 +08:00
commit 50b84ae88c
6 changed files with 472 additions and 0 deletions

63
README.md Normal file
View File

@@ -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

61
README_CN.md Normal file
View File

@@ -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

17
keygen.js Normal file
View File

@@ -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());

176
masar.py Normal file
View File

@@ -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)

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
jsbeautifier==1.14.0
jsmin==3.0.0
loguru==0.5.3
pycryptodome==3.11.0

151
typora.py Normal file
View File

@@ -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()