├── 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 --------------------------------------------------------------------------------