mirror of
https://github.com/Mas0nShi/typoraCracker.git
synced 2023-07-10 13:41:20 +08:00
Initial commit
This commit is contained in:
63
README.md
Normal file
63
README.md
Normal 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
61
README_CN.md
Normal 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
17
keygen.js
Normal 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
176
masar.py
Normal 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
4
requirements.txt
Normal 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
151
typora.py
Normal 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()
|
||||
Reference in New Issue
Block a user