├── Download.py
├── README.md
├── decrypt.py
└── information.xls
/Download.py:
--------------------------------------------------------------------------------
1 | # This Python file uses the following encoding: utf-8
2 | import os
3 | import subprocess
4 | import mysql.connector
5 | import requests
6 |
7 | headers = {
8 | # 填入伪造的IP地址
9 | 'X-Forwarded-For': '*.*.*.*'
10 | }
11 |
12 | FilePath = "E:/Project/Python/爬虫/科学文库/book/"
13 | surl = input("请输入书籍页链接:")
14 | value = surl.split('=')[1]
15 | # print(value)
16 | url = "https://book.sciencereading.cn/shop/book/Booksimple/offlineDownload.do?id=" + value +"&readMark=1"
17 | # print(url)
18 |
19 | mydb = mysql.connector.connect(
20 | host="127.0.0.1",
21 | user="root",
22 | password="root",
23 | database="科学文库"
24 | )
25 |
26 | mycursor = mydb.cursor()
27 |
28 | SQL = "SELECT 图书名称 FROM information WHERE value = " + "\'" + value + "\'"
29 | # print(SQL)
30 | mycursor.execute(SQL)
31 |
32 | myresult = mycursor.fetchall()[0][0]
33 | myresult = myresult.replace('|', '_')
34 | myresult = myresult.replace(' ', '')
35 | print(f"正在下载《{myresult}》")
36 |
37 | res = requests.get(url, headers=headers)
38 |
39 | if res.status_code == 200:
40 | with open(f"book/{myresult}.pdf", "wb") as f:
41 | f.write(res.content)
42 | print("图书下载完成")
43 | command = f"python decrypt.py -i {FilePath}{myresult}.pdf -o {FilePath}{myresult}_dec.pdf"
44 | # output = os.popen(command, 'r').readlines()
45 | process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
46 | process.wait()
47 | output = process.stdout.read()
48 | error = process.stderr.read()
49 | output = output.decode(encoding="gbk")
50 | error = error.decode(encoding="gbk")
51 | print(output)
52 | os.remove(f"{FilePath}{myresult}.pdf")
53 | print("加密PDF文件已删除")
54 | os.rename(f"{FilePath}{myresult}_dec.pdf", f"{FilePath}{myresult}.pdf")
55 | print("PDF文件已重命名")
56 | else:
57 | print(f"图书下载失败: {res.status_code}")
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [科学文库(ScienceReading)](https://book.sciencereading.cn/shop/main/Login/shopFrame.do)自动化下载解密工具
2 |
3 | 使用说明:
4 | - information.xls文件是科学文库网站所有的书籍目录(截止到2023年3月28日)
5 | - Python脚本中使用了MySQL 8.0版本数据库,建议安装MySQL 8.0和Navicat 16 for MySQL(可视化数据库管理工具),连接到默认数据库后创建“科学文库”数据库,导入information.xls,生成一个表,重命名为information,也可以更改代码在xls文件中检索,但是效率不高
6 | - decrypt.py来源与GitHub的另一个仓库[ScienceDecrypting](https://github.com/skq1998/ScienceDecrypting),ScienceDecrypting.exe是打包好的解密工具,带有GUI界面,来自已经删除的GitHub项目,可以选择文件进行解密
7 | - Download.py中的FilePath是下载的书籍要保存的路径,记得改成自己的,并注意路径中“\”要替换成“\\”或者“/”
8 | - 本工具唯一有点儿意义的代码就是伪造IP进行书籍下载,IP可以替换为各大高校或机构的IP,可以在通过CARSI登录科学文库的界面找到支持的高校或机构,通过ping域名的方式找到IP地址填入即可
9 |
--------------------------------------------------------------------------------
/decrypt.py:
--------------------------------------------------------------------------------
1 | # This Python file uses the following encoding: utf-8
2 |
3 | import base64
4 | import sys
5 | import traceback
6 | import requests
7 | import os
8 | import re
9 | import hashlib
10 | import tempfile
11 | from xml.etree import ElementTree
12 | from optparse import OptionParser
13 | import hashlib
14 | from cryptography.hazmat.primitives.ciphers import Cipher, modes, algorithms
15 | from cryptography.hazmat.primitives import padding
16 | from pikepdf import Pdf
17 |
18 | req_data = """
19 |
20 | {}
21 |
22 |
23 | """
24 | iv_first = b"200CFC8299B84aa980E945F63D3EF48D"
25 | iv_first = iv_first[:16]
26 |
27 |
28 | class CustomException(Exception):
29 | pass
30 |
31 |
32 | def aes_decrypt(key, iv, data, pad=False):
33 | cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
34 | dec = cipher.decryptor()
35 | ret = dec.update(data) + dec.finalize()
36 | if not pad:
37 | return ret
38 | unpadder = padding.PKCS7(128).unpadder()
39 | return unpadder.update(ret) + unpadder.finalize()
40 |
41 |
42 | def request_password(url, file_id):
43 | r = requests.post(url, headers={
44 | "User-Agent": "Readerdex 2.0",
45 | "Cache-Control": "no-cache"
46 | }, data=req_data.format(file_id))
47 | if r.status_code != 200:
48 | raise CustomException(
49 | "服务器异常,请稍后再试, file id: {}".format(file_id))
50 | try:
51 | root = ElementTree.fromstring(r.text)
52 | except Exception:
53 | raise CustomException(
54 | "invilid response, file id: {}".format(file_id))
55 | password = root.find("./password").text
56 | if not password or not password.strip():
57 | raise CustomException(
58 | "无法获取密码,文件可能已过期, file id:{}".format(file_id))
59 | return password.strip()
60 |
61 |
62 | def decrypt_file_key(password_from_file, password_from_server, iv_from_file, right_meta, rights):
63 | pass_dec = aes_decrypt(password_from_server, iv_first,
64 | base64.b64decode(password_from_file))
65 | m = hashlib.sha256()
66 | m.update(pass_dec[:0x20])
67 | m.update(right_meta)
68 | sha256 = m.digest()
69 | iv_second = base64.b64decode(iv_from_file)
70 | rights_dec = aes_decrypt(sha256, iv_second[:16], base64.b64decode(rights))
71 | m = re.search(r"([0-9a-f]+)",
72 | rights_dec.decode("utf-8"))
73 | if not m:
74 | raise CustomException("fail to get encrypt key: {}", rights_dec)
75 | pass_in_rights = m.group(1)
76 | pass_in_rights += "AppendCA"
77 | m = hashlib.sha1()
78 | m.update(pass_in_rights.encode("utf-8"))
79 | return m.digest()[:0x10]
80 |
81 |
82 | def decrypt_file(src, dest):
83 | print("[Log] 解析源文件....")
84 | with open(src, "rb") as fp:
85 | # find rights position
86 | fp.seek(0, os.SEEK_END)
87 | fp.seek(fp.tell() - 30, os.SEEK_SET)
88 | tail = fp.read()
89 | m = re.search(rb"startrights (\d+),(\d+)", tail)
90 | if not m:
91 | raise CustomException("文件格式错误 {}".format(tail))
92 | # find rights
93 | fp.seek(int(m.group(1)), os.SEEK_SET)
94 | eof_offset = int(m.group(1)) - 13
95 | right_meta = fp.read(int(m.group(2))).decode("latin")
96 | # request stage 1 password
97 | root = ElementTree.fromstring(right_meta)
98 | drm_url = root.find("./protect/auth/permit/server/url").text
99 | file_id = root.find("./file-id").text
100 | password_from_file = root.find("./protect/auth/permit/password").text
101 | iv_from_file = root.find("./protect/auth/iv").text
102 | rights = root.find("./rights").text
103 | stripped_right_meta = re.sub(
104 | r"\[\w+/=]+\", "", right_meta)
105 |
106 | print("[Log] 请求密钥...")
107 | password_from_server = request_password(drm_url, file_id)
108 |
109 | print("[Log] 解密DRM信息...")
110 | file_key = decrypt_file_key(password_from_file,
111 | password_from_server.encode("ascii"),
112 | iv_from_file,
113 | stripped_right_meta.encode("ascii"),
114 | rights)
115 | print("[Log] 解密文件...")
116 | src_fp = open(src, "rb")
117 | temp_fp = tempfile.TemporaryFile()
118 |
119 | # fix pdf format
120 | src_fp.seek(eof_offset - 40, os.SEEK_SET)
121 | content = src_fp.read(40)
122 | m = re.search(rb'startxref\s+(\d+)\s', content)
123 | if not m:
124 | raise CustomException("unable to find xref")
125 | src_fp.seek(0, os.SEEK_SET)
126 | temp_fp.write(src_fp.read(int(m.group(1)) - 512))
127 | encryption_obj = b"< /U <1> /P -4 /CF << /StdCF << /Type /CryptAlgorithm /CFM /AESV2 /AuthEvent /DocOpen >> >> /StrF /StdCF /StmF /StdCF>>"
128 | for line in src_fp:
129 | if b"%%EOF" in line:
130 | temp_fp.write(b"%%EOF")
131 | break
132 | if b"SubFilter/TTKN.PubSec.s1" in line:
133 | origin_len = len(line)
134 | line = encryption_obj + b"\n" * (origin_len - len(encryption_obj))
135 | temp_fp.write(line)
136 | src_fp.close()
137 | temp_fp.seek(0, os.SEEK_SET)
138 | out = open(dest, "wb")
139 |
140 | print("[Log] 写入文件")
141 | Pdf.open(temp_fp, password=file_key.hex(), hex_password=True).save(out)
142 | temp_fp.close()
143 | out.close()
144 | print("[Success] 解密成功!")
145 |
146 |
147 | def main():
148 | parser = OptionParser(
149 | usage="Usage: python3 %prog -i INPUT_FILE -o OUTPUT_FILE")
150 | parser.add_option("-i", "--input", dest="src",
151 | help="原始文件名", metavar="FILE")
152 | parser.add_option("-o", "--ouput", dest="dst",
153 | help="输出文件名", metavar="FILE")
154 | (options, _) = parser.parse_args()
155 | if not options.src or not options.dst:
156 | parser.print_help()
157 | exit(0)
158 | if not os.path.isfile(options.src):
159 | print("输入文件不存在")
160 | parser.print_help()
161 | exit(0)
162 | if os.path.isfile(options.dst):
163 | ans = input("文件 {} 已存在,继续运行将覆盖该文件,是否继续 [y/N]: ".format(options.dst))
164 | if ans.lower() not in ["y", "yes"]:
165 | exit(0)
166 |
167 | decrypt_file(options.src, options.dst)
168 |
169 |
170 | if __name__ == "__main__":
171 | try:
172 | main()
173 | except KeyboardInterrupt:
174 | print("Killed by user")
175 | sys.exit(0)
176 | except (CustomException, Exception) as exc:
177 | if not isinstance(exc, CustomException):
178 | print("[Error] 未知错误: ", str(exc))
179 | else:
180 | print("[Error]", str(exc))
181 | print("-" * 64)
182 | traceback.print_exc()
183 |
--------------------------------------------------------------------------------
/information.xls:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xwbperson/ScienceDecrypt/f727c391c0df4b89cabf1296e782cce3a175f659/information.xls
--------------------------------------------------------------------------------