├── README.md └── test.py /README.md: -------------------------------------------------------------------------------- 1 | # demoo 2 | ## UPDATE 3 | The decrypting of readmoo has been changed thoroughly, and this project is in deprecated. 4 | ## Introduce 5 | Decrypt your epub files from Readmoo Library. 6 | ## Environment 7 | Mac os, windows is not supported. 8 | ## How To Use 9 | ``` 10 | 1. Install readmoo app (desktop version) 11 | 2. Login your account on desktop app 12 | 3. run the python script 13 | ``` 14 | ## License 15 | This project is licensed under the MIT License 16 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #-*- coding: utf8 -*- 3 | import os 4 | import sys 5 | from os.path import expanduser 6 | import sqlite3 7 | import json 8 | from io import StringIO 9 | from Crypto.PublicKey import RSA 10 | 11 | homePath = expanduser('~') 12 | booksPath = 'books' 13 | downloadPath = 'download' 14 | deDrmOutputPath = 'deDrmBooks' 15 | 16 | if sys.platform == 'darwin': 17 | dbPath = homePath + '/Library/Application Support/Readmoo/Local Storage/' + 'app_readmoo_0.localstorage' 18 | elif sys.platform == 'win32': 19 | dbPath = homePath + '/AppData/Local/Readmoo/Local Storage/app_readmoo_0.localstorage' 20 | else: 21 | print('not support for ', sys.platform, '. return.') 22 | sys.exit() 23 | 24 | epubPathDownload = None 25 | rsaPrivateKey = None 26 | userId = None 27 | userToken = None 28 | userBooksLibraryData = None 29 | currentBooksInLibrary = {} 30 | toDecryptLocations = {} 31 | clientId = '8bb43bdd60795d800b16eec7b73abb80' 32 | epubDownloadUrl = 'https://api.readmoo.com/epub/' 33 | 34 | def getEpubUrl(bookId): 35 | return epubDownloadUrl + bookId + '?client_id='+ clientId + '&access_token=' +userToken 36 | 37 | def downloadEpub(bookId): 38 | import urllib.request, urllib.error, urllib.parse 39 | 40 | url = getEpubUrl(bookId) 41 | downloadToPath = os.path.join(downloadPath, userId) 42 | downloadToFileName = bookId + '.epub' 43 | outputFile = os.path.join(downloadToPath, downloadToFileName) 44 | if os.path.exists(outputFile): 45 | print(outputFile, ' is exists. return.') 46 | return 47 | 48 | print('open link: ', url) 49 | response = urllib.request.urlopen(url) 50 | 51 | #check folder 52 | if not os.path.exists(downloadToPath): 53 | os.makedirs(downloadToPath) 54 | 55 | print('create file: ', outputFile) 56 | file = open(outputFile, 'wb') 57 | print('downloading...') 58 | file.write(response.read()) 59 | print('downloading finished.') 60 | 61 | 62 | def extractEpub(bookId): 63 | import zipfile 64 | 65 | epubFile = os.path.join(downloadPath, userId, bookId + '.epub') 66 | toPath = os.path.join(os.path.dirname(epubFile), bookId) 67 | if not os.path.exists(toPath): 68 | os.makedirs(toPath) 69 | zf = zipfile.ZipFile(epubFile, mode='r') 70 | print('extract epub file...') 71 | zf.extractall(toPath) 72 | zf.close() 73 | 74 | 75 | def decryptAESKey(privateKey, cipherText): 76 | from Crypto.Cipher import PKCS1_v1_5 77 | from Crypto.PublicKey import RSA 78 | from Crypto import Random 79 | from Crypto.Hash import SHA 80 | import base64 81 | 82 | cipherText = base64.b64decode(cipherText) 83 | dsize = SHA.digest_size 84 | sentinel = Random.new().read(15+dsize) 85 | key = RSA.importKey(privateKey) 86 | rsaCipher = PKCS1_v1_5.new(key) 87 | decryptPvKey = rsaCipher.decrypt(cipherText, sentinel) 88 | return decryptPvKey 89 | 90 | def decryptFiles(aesKey, bookId, encryptedFiles, outputRootPath): 91 | from Crypto.Cipher import AES 92 | 93 | rootPath = os.path.join(epubPathDownload, bookId) 94 | outputBookPath = outputRootPath + '/' + bookId 95 | print('aes key ', str(aesKey)) 96 | print('input path ', rootPath) 97 | print('out path ', outputBookPath) 98 | 99 | for file in encryptedFiles: 100 | filePath = rootPath + '/' + file 101 | outputFile = outputBookPath + '/' + file 102 | fileName = os.path.basename(filePath) 103 | dirName = os.path.dirname(outputFile) 104 | if not os.path.exists(dirName): 105 | os.makedirs(dirName) 106 | 107 | f = open(filePath, 'rb') 108 | inputBytes = f.read() 109 | iv = inputBytes[:16] 110 | cipherText = inputBytes[16:] 111 | aesCipher = AES.new(aesKey, AES.MODE_CBC, iv) 112 | print('decrypting file: ', fileName) 113 | decryptBytes = aesCipher.decrypt(cipherText) 114 | decryptBytesLen = len(decryptBytes) 115 | #remove padding 116 | lastByte = decryptBytes[decryptBytesLen-1: decryptBytesLen] 117 | paddingLength = 0 118 | print('last 16 byte: ', str(decryptBytes[decryptBytesLen-16: decryptBytesLen])) 119 | # print('last byte: ', lastByte.encode('hex')) 120 | for index in range(decryptBytesLen-1, decryptBytesLen-17, -1): 121 | if decryptBytes[index] == lastByte: 122 | paddingLength += 1 123 | else: 124 | break; 125 | print('padding: ', str(lastByte), ' padding length: ', paddingLength) 126 | 127 | 128 | print('add new file: ', outputFile) 129 | f = open(outputFile, 'wb') 130 | f.write(decryptBytes[:decryptBytesLen-paddingLength]) 131 | print('decrypt ' + file + ' is finished.') 132 | 133 | def checkOtherFiles(bookId, outputRootPath): 134 | rootPath = os.path.join(epubPathDownload, bookId) 135 | outPath = outputRootPath + '/' + bookId 136 | 137 | for r, d, f in os.walk(rootPath): 138 | for name in d: 139 | outputDir = os.path.join(r, name) 140 | outputDir = os.path.relpath(outputDir, rootPath) 141 | outputDir = os.path.join(outPath, outputDir) 142 | if not os.path.exists(outputDir): 143 | #create folder 144 | print('create folder: ', outputDir) 145 | os.makedirs(outputDir) 146 | for name in f: 147 | basename = os.path.basename(name) 148 | if basename != 'encryption.xml': 149 | outputFile = os.path.join(r, name) 150 | outputFile = os.path.relpath(outputFile, rootPath) 151 | outputFile = os.path.join(outPath, outputFile) 152 | if not os.path.exists(outputFile): 153 | #copy file 154 | srcFile = os.path.join(r, name) 155 | file = open(srcFile, 'rb') 156 | inputData = file.read() 157 | print('create file: ', outputFile) 158 | file = open(outputFile, 'wb') 159 | file.write(inputData) 160 | 161 | def outputDecryptedEpubFile(bookId, outputPath): 162 | import zipfile 163 | outputZipFilePath = outputPath + '/' + bookId + '.epub' 164 | booksPath = outputPath + '/' + bookId 165 | print('create zip file: ', outputZipFilePath) 166 | zf = zipfile.ZipFile(outputZipFilePath, mode='w') 167 | for r, d, f in os.walk(booksPath): 168 | for name in f: 169 | filePath = os.path.join(r, name) 170 | zipFilePath = os.path.relpath(filePath, booksPath) 171 | file = open(filePath, 'rb') 172 | fileByte = file.read() 173 | zf.writestr(zipFilePath, fileByte) 174 | 175 | print(zf.printdir()) 176 | zf.close() 177 | 178 | def decryptBook(bookId): 179 | import xml.etree.ElementTree as ET 180 | 181 | #find encryption.xml 182 | encryptonFilePath = 'META-INF/encryption.xml' 183 | bookTopPath = os.path.join(epubPathDownload, bookId) 184 | xmlTree = ET.parse(os.path.join(bookTopPath, encryptonFilePath)) 185 | root = xmlTree.getroot() 186 | #get the cipher of encrypted key 187 | mainEncryptedKey = root.find('{http://www.w3.org/2001/04/xmlenc#}EncryptedKey') 188 | for child in mainEncryptedKey.iter('{http://www.w3.org/2001/04/xmlenc#}CipherValue'): 189 | mainEncryptedKey = child.text 190 | 191 | #get all cipher ref of encrypted files 192 | encryptedFilesReference = [] 193 | for child in root.iter('{http://www.w3.org/2001/04/xmlenc#}CipherReference'): 194 | encryptedFilesReference.append(child.attrib['URI']) 195 | 196 | #decrypt aes key 197 | aesKey = decryptAESKey(rsaPrivateKey, mainEncryptedKey) 198 | 199 | decryptFiles(aesKey, bookId, encryptedFilesReference, 'output') 200 | print('checking other files...') 201 | checkOtherFiles(bookId, 'output') 202 | outputDecryptedEpubFile(bookId, 'output') 203 | 204 | #read db from readmoo 205 | conn = sqlite3.connect(dbPath) 206 | cursor = conn.execute('SELECT * FROM ItemTable') 207 | for row in cursor: 208 | if row[0] == 'rsa_privateKey': 209 | rsaPrivateKey = row[1].decode('utf16') 210 | elif row[0] == '-nw-library': 211 | userBooksLibraryData = row[1].decode('utf16') 212 | elif row[0] == '-nw-access_token': 213 | userToken = row[1].decode('utf16') 214 | elif row[0] == '-nw-userid': 215 | userId = row[1].decode('utf16') 216 | conn.close() 217 | 218 | epubPathDownload = os.path.join(downloadPath, userId) 219 | 220 | #parse book info 221 | io = StringIO(userBooksLibraryData) 222 | userBooksLibraryData = json.load(io) 223 | for item in userBooksLibraryData: 224 | bookId = item['library_item']['book']['id'] 225 | title = item['library_item']['book']['title'] 226 | currentBooksInLibrary.update({bookId: title}) 227 | 228 | allBooksId = list(currentBooksInLibrary.keys()) 229 | if len(allBooksId) <= 0: 230 | print('there is no book to encrypt. finish.') 231 | sys.exit() 232 | else: 233 | print('user id: ', userId) 234 | print('all your books:') 235 | #print(books list 236 | for index in range(0, len(allBooksId)): 237 | bookId = allBooksId[index] 238 | print(' #', index, currentBooksInLibrary[bookId], '(', bookId, ')' ) 239 | print('') 240 | 241 | inputNumber = -1 242 | 243 | while inputNumber < 0 or inputNumber >= len(allBooksId): 244 | inputNumber = int(eval(input('input the number to decrypt: '))) 245 | #process the book 246 | choosedBookId = allBooksId[inputNumber] 247 | 248 | downloadEpub(choosedBookId) 249 | extractEpub(choosedBookId) 250 | decryptBook(choosedBookId) 251 | --------------------------------------------------------------------------------