SHOW:
|
|
- or go back to the newest paste.
| 1 | - | #!/usr/bin/python |
| 1 | + | #!/usr/bin/env python |
| 2 | # | |
| 3 | # Updates July 2012 by Michael Newton | |
| 4 | # PWSD ID is no longer a MAC address, but should always | |
| 5 | - | # 8====D~~~~ O: # |
| 5 | + | # be stored in the registry. Script now works with OS X |
| 6 | # and checks plist for values instead of registry. Must | |
| 7 | # have biplist installed for OS X support. | |
| 8 | # | |
| 9 | ########################################################## | |
| 10 | # KOBO DRM CRACK BY # | |
| 11 | # PHYSISTICATED # | |
| 12 | ########################################################## | |
| 13 | # This app was made for Python 2.7 on Windows 32-bit | |
| 14 | # | |
| 15 | # This app needs pycrypto - get from here: | |
| 16 | # http://www.voidspace.org.uk/python/modules.shtml | |
| 17 | # | |
| 18 | # Usage: obok.py | |
| 19 | # Choose the book you want to decrypt | |
| 20 | # | |
| 21 | # Shouts to my krew - you know who you are - and one in | |
| 22 | # particular who gave me a lot of help with this - thank | |
| 23 | # you so much! | |
| 24 | # | |
| 25 | # Kopimi /K\ | |
| 26 | # Keep sharing, keep copying, but remember that nothing is | |
| 27 | # for free - make sure you compensate your favorite | |
| 28 | # authors - and cut out the middle man whenever possible | |
| 29 | # ;) ;) ;) | |
| 30 | # | |
| 31 | # DRM AUTOPSY | |
| 32 | # The Kobo DRM was incredibly easy to crack, but it took | |
| 33 | # me months to get around to making this. Here's the | |
| 34 | # basics of how it works: | |
| 35 | # 1: Get MAC address of first NIC in ipconfig (sometimes | |
| 36 | # stored in registry as pwsdid) | |
| 37 | # 2: Get user ID (stored in tons of places, this gets it | |
| 38 | # from HKEY_CURRENT_USER\Software\Kobo\Kobo Desktop | |
| 39 | # Edition\Browser\cookies) | |
| 40 | # 3: Concatenate and SHA256, take the second half - this | |
| 41 | # is your master key | |
| 42 | # 4: Open %LOCALAPPDATA%\Kobo Desktop Editions\Kobo.sqlite | |
| 43 | # and dump content_keys | |
| 44 | # 5: Unbase64 the keys, then decode these with the master | |
| 45 | # key - these are your page keys | |
| 46 | # 6: Unzip EPUB of your choice, decrypt each page with its | |
| 47 | # page key, then zip back up again | |
| 48 | # | |
| 49 | # WHY USE THIS WHEN INEPT WORKS FINE? (adobe DRM stripper) | |
| 50 | # Inept works very well, but authors on Kobo can choose | |
| 51 | # what DRM they want to use - and some have chosen not to | |
| 52 | # let people download them with Adobe Digital Editions - | |
| 53 | # they would rather lock you into a single platform. | |
| 54 | # | |
| 55 | # With Obok, you can sync Kobo Desktop, decrypt all your | |
| 56 | # ebooks, and then use them on whatever device you want | |
| 57 | # - you bought them, you own them, you can do what you | |
| 58 | # like with them. | |
| 59 | # | |
| 60 | # Obok is Kobo backwards, but it is also means "next to" | |
| 61 | # in Polish. | |
| 62 | # When you buy a real book, it is right next to you. You | |
| 63 | # can read it at home, at work, on a train, you can lend | |
| 64 | # it to a friend, you can scribble on it, and add your own | |
| 65 | # explanations/translations. | |
| 66 | # | |
| 67 | # Obok gives you this power over your ebooks - no longer | |
| 68 | # are you restricted to one device. This allows you to | |
| 69 | # embed foreign fonts into your books, as older Kobo's | |
| 70 | # can't display them properly. You can read your books | |
| 71 | # on your phones, in different PC readers, and different | |
| 72 | - | import _winreg |
| 72 | + | |
| 73 | # too, if you like - you can do that with a real book | |
| 74 | # after all. | |
| 75 | # | |
| 76 | """ | |
| 77 | Decrypt Kobo encrypted EPUB books. | |
| 78 | """ | |
| 79 | ||
| 80 | import os | |
| 81 | - | from uuid import getnode as get_mac |
| 81 | + | import sys |
| 82 | if sys.platform.startswith('win'):
| |
| 83 | import _winreg | |
| 84 | - | return hashlib.sha256(raw).hexdigest() |
| 84 | + | elif sys.platform.startswith('darwin'):
|
| 85 | from biplist import readPlist | |
| 86 | import re | |
| 87 | - | lastchar = binascii.b2a_hex(contents[-1:]) |
| 87 | + | |
| 88 | - | len = int(lastchar, 16) |
| 88 | + | |
| 89 | - | padding = len |
| 89 | + | |
| 90 | - | if(len == 1): |
| 90 | + | |
| 91 | - | return contents[:-1] |
| 91 | + | |
| 92 | - | if(len < 16): |
| 92 | + | |
| 93 | - | for i in range(len): |
| 93 | + | |
| 94 | - | testchar = binascii.b2a_hex(contents[-len:-(len-1)]) |
| 94 | + | |
| 95 | - | if(testchar != lastchar): |
| 95 | + | |
| 96 | - | padding = 0 |
| 96 | + | return hashlib.sha256(raw).hexdigest() |
| 97 | - | if(padding > 0): |
| 97 | + | |
| 98 | - | contents = contents[:-padding] |
| 98 | + | |
| 99 | - | return contents |
| 99 | + | lastchar = binascii.b2a_hex(contents[-1:]) |
| 100 | - | |
| 100 | + | strlen = int(lastchar, 16) |
| 101 | - | def GetVolumeKeys(c): |
| 101 | + | padding = strlen |
| 102 | - | volumekeys = {}
|
| 102 | + | if(strlen == 1): |
| 103 | - | for row in c.execute("SELECT * from content_keys"):
|
| 103 | + | return contents[:-1] |
| 104 | - | if(row[0] not in volumekeys): |
| 104 | + | if(strlen < 16): |
| 105 | - | volumekeys[row[0]] = {}
|
| 105 | + | for i in range(strlen): |
| 106 | - | volumekeys[row[0]][row[1]] = {}
|
| 106 | + | testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)]) |
| 107 | - | volumekeys[row[0]][row[1]]["encryptedkey"] = base64.b64decode(row[2]) |
| 107 | + | if(testchar != lastchar): |
| 108 | - | volumekeys[row[0]][row[1]]["decryptedkey"] = enc.decrypt(volumekeys[row[0]][row[1]]["encryptedkey"]) |
| 108 | + | padding = 0 |
| 109 | - | # get book name |
| 109 | + | if(padding > 0): |
| 110 | - | for key in volumekeys.keys(): |
| 110 | + | contents = contents[:-padding] |
| 111 | - | volumekeys[key]["title"] = c.execute("SELECT Title from content where ContentID = '%s'" % (key)).fetchone()[0]
|
| 111 | + | return contents |
| 112 | - | return volumekeys |
| 112 | + | |
| 113 | def GetVolumeKeys(dbase, enc): | |
| 114 | volumekeys = {}
| |
| 115 | - | def GetMacString(): |
| 115 | + | for row in dbase.execute("SELECT * from content_keys"):
|
| 116 | - | # terrible way, but get_mac() only returns proper interfaces |
| 116 | + | if(row[0] not in volumekeys): |
| 117 | - | # this will return the first in ipconfig |
| 117 | + | volumekeys[row[0]] = {}
|
| 118 | - | # Kobo appears to do the same |
| 118 | + | volumekeys[row[0]][row[1]] = {}
|
| 119 | - | ipconfig = os.popen('ipconfig /all').read()
|
| 119 | + | volumekeys[row[0]][row[1]]["encryptedkey"] = base64.b64decode(row[2]) |
| 120 | - | c = re.compile("Physical Address[^0-9A-F]+([0-9A-Fa-f-]+)", re.MULTILINE)
|
| 120 | + | volumekeys[row[0]][row[1]]["decryptedkey"] = enc.decrypt(volumekeys[row[0]][row[1]]["encryptedkey"]) |
| 121 | - | m = c.search(ipconfig) |
| 121 | + | # get book name |
| 122 | - | mac = re.sub("-", ":", m.group(1))
|
| 122 | + | for key in volumekeys.keys(): |
| 123 | - | return mac |
| 123 | + | volumekeys[key]["title"] = dbase.execute("SELECT Title from content where ContentID = '%s'" % (key)).fetchone()[0]
|
| 124 | return volumekeys | |
| 125 | - | #macnum = get_mac() |
| 125 | + | |
| 126 | - | #macarray = [] |
| 126 | + | def ByteArrayToString(bytearr): |
| 127 | - | #for i in range(6): |
| 127 | + | wincheck = re.match("@ByteArray\\((.+)\\)", bytearr)
|
| 128 | - | # macarray.append((hex((macnum >> (40-(i*8)) & 0xFF))[2:-1]).zfill(2)) |
| 128 | + | if wincheck: |
| 129 | - | #return string.upper(string.join(macarray, ":")) |
| 129 | + | return wincheck.group(1) |
| 130 | return bytearr | |
| 131 | - | def ByteArrayToString(bytearray): |
| 131 | + | |
| 132 | - | return re.match("@ByteArray\\((.+)\\)", bytearray).group(1)
|
| 132 | + | def GetUserHexKey(prefs = ""): |
| 133 | "find wsuid and pwsdid" | |
| 134 | - | def GetUserHexKey(): |
| 134 | + | wsuid = "" |
| 135 | - | regkey_browser = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Software\\Kobo\\Kobo Desktop Edition\\Browser") |
| 135 | + | pwsdid = "" |
| 136 | - | cookies = _winreg.QueryValueEx(regkey_browser, "cookies") |
| 136 | + | if sys.platform.startswith('win'):
|
| 137 | - | bytearrays = cookies[0] |
| 137 | + | regkey_browser = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Software\\Kobo\\Kobo Desktop Edition\\Browser") |
| 138 | - | # find wsuid and pwsdid |
| 138 | + | cookies = _winreg.QueryValueEx(regkey_browser, "cookies") |
| 139 | - | # pwsdid isn't always stored, but GetMacString() will get it for us |
| 139 | + | bytearrays = cookies[0] |
| 140 | - | wsuid = "" |
| 140 | + | elif sys.platform.startswith('darwin'):
|
| 141 | - | pwsdid = GetMacString() |
| 141 | + | cookies = readPlist(prefs) |
| 142 | - | for bytearray in bytearrays: |
| 142 | + | bytearrays = cookies["Browser.cookies"] |
| 143 | - | cookie = ByteArrayToString(bytearray) |
| 143 | + | for bytearr in bytearrays: |
| 144 | - | wsuidcheck = re.match("^wsuid=([0-9a-f-]+)", cookie)
|
| 144 | + | cookie = ByteArrayToString(bytearr) |
| 145 | - | if(wsuidcheck): |
| 145 | + | print cookie |
| 146 | - | wsuid = wsuidcheck.group(1) |
| 146 | + | wsuidcheck = re.match("^wsuid=([0-9a-f-]+)", cookie)
|
| 147 | - | if(wsuid == ""): |
| 147 | + | if(wsuidcheck): |
| 148 | - | print "wsuid key not found :/" |
| 148 | + | wsuid = wsuidcheck.group(1) |
| 149 | - | exit() |
| 149 | + | pwsdidcheck = re.match("^pwsdid=([0-9a-f-]+)", cookie)
|
| 150 | - | preuserkey = string.join((pwsdid, wsuid), "") |
| 150 | + | if (pwsdidcheck): |
| 151 | - | userkey = SHA256(preuserkey) |
| 151 | + | pwsdid = pwsdidcheck.group(1) |
| 152 | - | return userkey[32:] |
| 152 | + | |
| 153 | if(wsuid == "" or pwsdid == ""): | |
| 154 | print "wsuid or pwsdid key not found :/" | |
| 155 | - | userkeyhex = GetUserHexKey() |
| 155 | + | exit() |
| 156 | preuserkey = string.join((pwsdid, wsuid), "") | |
| 157 | print SHA256(pwsdid) | |
| 158 | userkey = SHA256(preuserkey) | |
| 159 | return userkey[32:] | |
| 160 | - | kobodir = string.join((os.environ['LOCALAPPDATA'], "Kobo\\Kobo Desktop Edition"), "\\") |
| 160 | + | |
| 161 | - | sqlitefile = string.join((kobodir, "Kobo.sqlite"), "\\") |
| 161 | + | |
| 162 | - | bookdir = string.join((kobodir, "kepub"), "\\") |
| 162 | + | if sys.platform.startswith('win'):
|
| 163 | delim = "\\" | |
| 164 | kobodir = string.join((os.environ['LOCALAPPDATA'], "Kobo\\Kobo Desktop Edition"), delim) | |
| 165 | elif sys.platform.startswith('darwin'):
| |
| 166 | - | c = conn.cursor() |
| 166 | + | delim = "/" |
| 167 | kobodir = string.join((os.environ['HOME'], "Library/Application Support/Kobo/Kobo Desktop Edition"), delim) | |
| 168 | - | volumekeys = GetVolumeKeys(c) |
| 168 | + | prefs = string.join((os.environ['HOME'], "Library/Preferences/com.kobo.Kobo Desktop Edition.plist"), delim) |
| 169 | sqlitefile = string.join((kobodir, "Kobo.sqlite"), delim) | |
| 170 | bookdir = string.join((kobodir, "kepub"), delim) | |
| 171 | ||
| 172 | # get key | |
| 173 | userkeyhex = GetUserHexKey(prefs) | |
| 174 | # load into AES | |
| 175 | userkey = binascii.a2b_hex(userkeyhex) | |
| 176 | - | print "%d: %s" % (i, volumekeys[key]["title"]) |
| 176 | + | |
| 177 | - | i+=1 |
| 177 | + | |
| 178 | # open sqlite | |
| 179 | conn = sqlite3.connect(sqlitefile) | |
| 180 | dbcursor = conn.cursor() | |
| 181 | # get volume keys | |
| 182 | volumekeys = GetVolumeKeys(dbcursor, enc) | |
| 183 | - | if(i==num): |
| 183 | + | |
| 184 | - | volumeid = key |
| 184 | + | |
| 185 | - | i+=1 |
| 185 | + | |
| 186 | volumeid = "" | |
| 187 | print "Choose a book to decrypt:" | |
| 188 | i = 1 | |
| 189 | - | exit() |
| 189 | + | |
| 190 | print "%d: %s" % (i, volumekeys[key]["title"]) | |
| 191 | - | zippath = string.join((bookdir, volumeid), "\\") |
| 191 | + | i += 1 |
| 192 | ||
| 193 | num = input("...")
| |
| 194 | ||
| 195 | - | outname = "%s.epub" % (re.sub("[^a-zA-Z0-9 ]", "", volumekeys[volumeid]["title"]))
|
| 195 | + | |
| 196 | for key in volumekeys.keys(): | |
| 197 | if(i == num): | |
| 198 | - | #print filename |
| 198 | + | volumeid = key |
| 199 | - | # read in and decrypt |
| 199 | + | i += 1 |
| 200 | - | if(filename in volumekeys[volumeid]): |
| 200 | + | |
| 201 | - | # do decrypted version |
| 201 | + | |
| 202 | - | pagekey = volumekeys[volumeid][filename]["decryptedkey"] |
| 202 | + | exit() |
| 203 | - | penc = AES.new(pagekey, AES.MODE_ECB) |
| 203 | + | |
| 204 | - | contents = RemoveAESPadding(penc.decrypt(z.read(filename))) |
| 204 | + | zippath = string.join((bookdir, volumeid), delim) |
| 205 | - | # need to fix padding |
| 205 | + | |
| 206 | - | zout.writestr(filename, contents) |
| 206 | + | |
| 207 | - | else: |
| 207 | + | |
| 208 | - | zout.writestr(filename, z.read(filename)) |
| 208 | + | outname = "%s.epub" % (re.sub("[^a-zA-Z0-9]", "", volumekeys[volumeid]["title"]))
|
| 209 | zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED) | |
| 210 | - | print "Book saved as %s\\%s" % (os.getcwd(), outname) |
| 210 | + | |
| 211 | #print filename | |
| 212 | # read in and decrypt | |
| 213 | if(filename in volumekeys[volumeid]): | |
| 214 | # do decrypted version | |
| 215 | pagekey = volumekeys[volumeid][filename]["decryptedkey"] | |
| 216 | penc = AES.new(pagekey, AES.MODE_ECB) | |
| 217 | contents = RemoveAESPadding(penc.decrypt(z.read(filename))) | |
| 218 | # need to fix padding | |
| 219 | zout.writestr(filename, contents) | |
| 220 | else: | |
| 221 | zout.writestr(filename, z.read(filename)) | |
| 222 | ||
| 223 | print "Book saved as %s%s%s" % (os.getcwd(), delim, outname) |