tools v6.2.0

Updated for B&N new scheme, added obok plugin, and many minor fixes,
This commit is contained in:
Apprentice Harper
2015-03-09 07:38:31 +00:00
committed by Apprentice Alf
parent c4fc10395b
commit 9d9c879413
54 changed files with 6345 additions and 604 deletions

View File

@@ -0,0 +1,335 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# ignoblekey.py
# Copyright © 2015 Apprentice Alf
# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Revision history:
# 1.0 - Initial release
"""
Get Barnes & Noble EPUB user key from nook Studio log file
"""
__license__ = 'GPL v3'
__version__ = "1.0"
import sys
import os
import hashlib
import getopt
import re
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"ignoblekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception):
pass
# Locate all of the nookStudy/nook for PC/Mac log file and return as list
def getNookLogFiles():
logFiles = []
found = False
if iswindows:
import _winreg as winreg
# some 64 bit machines do not have the proper registry key for some reason
# or the python interface to the 32 vs 64 bit registry is broken
paths = set()
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
if os.path.isdir(path):
paths.add(path)
if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Local"
if os.path.isdir(path):
paths.add(path)
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Roaming"
if os.path.isdir(path):
paths.add(path)
# User Shell Folders show take precedent over Shell Folders if present
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'AppData')[0]
if os.path.isdir(path):
paths.add(path)
except WindowsError:
pass
for path in paths:
# look for nookStudy log file
logpath = path +'\\Barnes & Noble\\NOOKstudy\\logs\\BNClientLog.txt'
if os.path.isfile(logpath):
found = True
print('Found nookStudy log file: ' + logpath.encode('ascii','ignore'))
logFiles.append(logpath)
else:
home = os.getenv('HOME')
# check for BNClientLog.txt in various locations
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/logs/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/indices/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/logs/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/indices/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
print('Found nookStudy log file: ' + testpath)
found = True
if not found:
print('No nook Study log files have been found.')
return logFiles
# Extract CCHash key(s) from log file
def getKeysFromLog(kLogFile):
keys = []
regex = re.compile("ccHash: \"(.{28})\"");
for line in open(kLogFile):
for m in regex.findall(line):
keys.append(m)
return keys
# interface for calibre plugin
def nookkeys(files = []):
keys = []
if files == []:
files = getNookLogFiles()
for file in files:
fileKeys = getKeysFromLog(file)
if fileKeys:
print u"Found {0} keys in the Nook Study log files".format(len(fileKeys))
keys.extend(fileKeys)
return keys
# interface for Python DeDRM
# returns single key or multiple keys, depending on path or file passed in
def getkey(outpath, files=[]):
keys = nookkeys(files)
if len(keys) > 0:
if not os.path.isdir(outpath):
outfile = outpath
with file(outfile, 'w') as keyfileout:
keyfileout.write(keys[0])
print u"Saved a key to {0}".format(outfile)
else:
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(outpath,u"nookkey{0:d}.b64".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
print u"Saved a key to {0}".format(outfile)
return True
return False
def usage(progname):
print u"Finds the nook Study encryption keys."
print u"Keys are saved to the current directory, or a specified output directory."
print u"If a file name is passed instead of a directory, only the first key is saved, in that file."
print u"Usage:"
print u" {0:s} [-h] [-k <logFile>] [<outpath>]".format(progname)
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__)
try:
opts, args = getopt.getopt(argv[1:], "hk:")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
usage(progname)
sys.exit(2)
files = []
for o, a in opts:
if o == "-h":
usage(progname)
sys.exit(0)
if o == "-k":
files = [a]
if len(args) > 1:
usage(progname)
sys.exit(2)
if len(args) == 1:
# save to the specified file or directory
outpath = args[0]
if not os.path.isabs(outpath):
outpath = os.path.abspath(outpath)
else:
# save to the same directory as the script
outpath = os.path.dirname(argv[0])
# make sure the outpath is the
outpath = os.path.realpath(os.path.normpath(outpath))
if not getkey(outpath, files):
print u"Could not retrieve nook Study key."
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import traceback
except:
return cli_main()
class ExceptionDialog(Tkinter.Frame):
def __init__(self, root, text):
Tkinter.Frame.__init__(self, root, border=5)
label = Tkinter.Label(self, text=u"Unexpected error:",
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
label.pack(fill=Tkconstants.X, expand=0)
self.text = Tkinter.Text(self)
self.text.pack(fill=Tkconstants.BOTH, expand=1)
self.text.insert(Tkconstants.END, text)
argv=unicode_argv()
root = Tkinter.Tk()
root.withdraw()
progpath, progname = os.path.split(argv[0])
success = False
try:
keys = nookkeys()
keycount = 0
for key in keys:
print key
while True:
keycount += 1
outfile = os.path.join(progpath,u"nookkey{0:d}.b64".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except DrmException, e:
tkMessageBox.showerror(progname, u"Error: {0}".format(str(e)))
except Exception:
root.wm_state('normal')
root.title(progname)
text = traceback.format_exc()
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
root.mainloop()
if not success:
return 1
return 0
if __name__ == '__main__':
if len(sys.argv) > 1:
sys.exit(cli_main())
sys.exit(gui_main())

View File

@@ -0,0 +1,11 @@
Notes from UE01 about this patch:
1. Revised the “Other_Tools/Kindle_for_Android_Patches/” for Kindle 4.8.1.10
2. Built Kindle 4.8.1.10 with the PID List added to the About activity
3. Uninstalled the Amazon/Play-store version and installed the patched version
4. Signed in to Amazon
5. Opened the book
6. Did Info > About > PID List and copied the PIDs to Calibres Plugins>File type > DeDRM > Mobipocket dialog
7. **Crucial** copied the PRC file to the PC (because the files checksum has changed since it was last copied)
8. In Calibre, Add Books (from a single directory)

View File

@@ -0,0 +1,157 @@
diff --git a/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali b/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
index 8ea400e..3aefad2 100644
--- a/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
+++ b/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
@@ -41,6 +41,8 @@
.field private security:Lcom/mobipocket/android/library/reader/AndroidSecurity;
+.field private pidList:Ljava/lang/String;
+
.field private totalMemory:J
@@ -74,6 +76,10 @@
.line 133
iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->security:Lcom/mobipocket/android/library/reader/AndroidSecurity;
+ const-string v0, "Open DRMed book to show PID list."
+
+ iput-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
.line 134
sget-object v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->TAG:Ljava/lang/String;
@@ -1339,3 +1345,26 @@
return-wide v0
.end method
+
+.method public getPidList()Ljava/lang/String;
+ .locals 1
+
+ .prologue
+ .line 15
+ iget-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
+ return-object v0
+.end method
+
+.method public setPidList(Ljava/lang/String;)V
+ .locals 0
+ .param p1, "value"
+
+ .prologue
+ .line 11
+ iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
+ .line 12
+ return-void
+.end method
+
diff --git a/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali b/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
index e4a3523..2269fab 100644
--- a/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
+++ b/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
@@ -30,3 +30,9 @@
.method public abstract getPid()Ljava/lang/String;
.end method
+
+.method public abstract getPidList()Ljava/lang/String;
+.end method
+
+.method public abstract setPidList(Ljava/lang/String;)V
+.end method
diff --git a/smali/com/amazon/kcp/info/AboutActivity.smali b/smali/com/amazon/kcp/info/AboutActivity.smali
index 5640e9e..e298341 100644
--- a/smali/com/amazon/kcp/info/AboutActivity.smali
+++ b/smali/com/amazon/kcp/info/AboutActivity.smali
@@ -493,6 +493,57 @@
return-void
.end method
+.method private populatePIDList()V
+ .locals 7
+
+ .prologue
+ .line 313
+ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider;
+
+ move-result-object v0
+
+ invoke-interface {v0}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->getPidList()Ljava/lang/String;
+
+ move-result-object v1
+
+ .line 314
+ .local v1, "PidList":Ljava/lang/String;
+ iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List;
+
+ new-instance v4, Lcom/amazon/kcp/info/AboutActivity$GroupItem;
+
+ const-string v5, "PID List"
+
+ const v6, 0x1
+
+ invoke-direct {v4, p0, v5, v6}, Lcom/amazon/kcp/info/AboutActivity$GroupItem;-><init>(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Z)V
+
+ invoke-interface {v3, v4}, Ljava/util/List;->add(Ljava/lang/Object;)Z
+
+ .line 315
+ new-instance v2, Ljava/util/ArrayList;
+
+ invoke-direct {v2}, Ljava/util/ArrayList;-><init>()V
+
+ .line 316
+ .local v2, "children":Ljava/util/List;,"Ljava/util/List<Lcom/amazon/kcp/info/AboutActivity$DetailItem;>;"
+ new-instance v3, Lcom/amazon/kcp/info/AboutActivity$DetailItem;
+
+ const-string v4, "PIDs"
+
+ invoke-direct {v3, p0, v4, v1}, Lcom/amazon/kcp/info/AboutActivity$DetailItem;-><init>(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Ljava/lang/String;)V
+
+ invoke-interface {v2, v3}, Ljava/util/List;->add(Ljava/lang/Object;)Z
+
+ .line 317
+ iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List;
+
+ invoke-interface {v3, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z
+
+ .line 318
+ return-void
+.end method
+
.method private populateDisplayItems()V
.locals 1
@@ -538,6 +589,8 @@
.line 173
invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateDisplayInformation()V
+ invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populatePIDList()V
+
.line 174
return-void
diff --git a/smali/com/amazon/system/security/Security.smali b/smali/com/amazon/system/security/Security.smali
index 04ea997..e88fe08 100644
--- a/smali/com/amazon/system/security/Security.smali
+++ b/smali/com/amazon/system/security/Security.smali
@@ -940,6 +940,16 @@
aput-object v0, v6, v8
+ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider;
+
+ move-result-object v5
+
+ invoke-static {v6}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String;
+
+ move-result-object v2
+
+ invoke-interface {v5, v2}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->setPidList(Ljava/lang/String;)V
+
.line 347
return-object v6
.end method

490
Other_Tools/Kobo/obok.py Normal file
View File

@@ -0,0 +1,490 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Version 3.1.2 January 2015
# Add coding, version number and version announcement
#
# Version 3.05 October 2014
# Identifies DRM-free books in the dialog
#
# Version 3.04 September 2014
# Handles DRM-free books as well (sometimes Kobo Library doesn't
# show download link for DRM-free books)
#
# Version 3.03 August 2014
# If PyCrypto is unavailable try to use libcrypto for AES_ECB.
#
# Version 3.02 August 2014
# Relax checking of application/xhtml+xml and image/jpeg content.
#
# Version 3.01 June 2014
# Check image/jpeg as well as application/xhtml+xml content. Fix typo
# in Windows ipconfig parsing.
#
# Version 3.0 June 2014
# Made portable for Mac and Windows, and the only module dependency
# not part of python core is PyCrypto. Major code cleanup/rewrite.
# No longer tries the first MAC address; tries them all if it detects
# the decryption failed.
#
# Updated September 2013 by Anon
# Version 2.02
# Incorporated minor fixes posted at Apprentice Alf's.
#
# Updates July 2012 by Michael Newton
# PWSD ID is no longer a MAC address, but should always
# be stored in the registry. Script now works with OS X
# and checks plist for values instead of registry. Must
# have biplist installed for OS X support.
#
# Original comments left below; note the "AUTOPSY" is inaccurate. See
# KoboLibrary.userkeys and KoboFile.decrypt()
#
##########################################################
# KOBO DRM CRACK BY #
# PHYSISTICATED #
##########################################################
# This app was made for Python 2.7 on Windows 32-bit
#
# This app needs pycrypto - get from here:
# http://www.voidspace.org.uk/python/modules.shtml
#
# Usage: obok.py
# Choose the book you want to decrypt
#
# Shouts to my krew - you know who you are - and one in
# particular who gave me a lot of help with this - thank
# you so much!
#
# Kopimi /K\
# Keep sharing, keep copying, but remember that nothing is
# for free - make sure you compensate your favorite
# authors - and cut out the middle man whenever possible
# ;) ;) ;)
#
# DRM AUTOPSY
# The Kobo DRM was incredibly easy to crack, but it took
# me months to get around to making this. Here's the
# basics of how it works:
# 1: Get MAC address of first NIC in ipconfig (sometimes
# stored in registry as pwsdid)
# 2: Get user ID (stored in tons of places, this gets it
# from HKEY_CURRENT_USER\Software\Kobo\Kobo Desktop
# Edition\Browser\cookies)
# 3: Concatenate and SHA256, take the second half - this
# is your master key
# 4: Open %LOCALAPPDATA%\Kobo Desktop Editions\Kobo.sqlite
# and dump content_keys
# 5: Unbase64 the keys, then decode these with the master
# key - these are your page keys
# 6: Unzip EPUB of your choice, decrypt each page with its
# page key, then zip back up again
#
# WHY USE THIS WHEN INEPT WORKS FINE? (adobe DRM stripper)
# Inept works very well, but authors on Kobo can choose
# what DRM they want to use - and some have chosen not to
# let people download them with Adobe Digital Editions -
# they would rather lock you into a single platform.
#
# With Obok, you can sync Kobo Desktop, decrypt all your
# ebooks, and then use them on whatever device you want
# - you bought them, you own them, you can do what you
# like with them.
#
# Obok is Kobo backwards, but it is also means "next to"
# in Polish.
# When you buy a real book, it is right next to you. You
# can read it at home, at work, on a train, you can lend
# it to a friend, you can scribble on it, and add your own
# explanations/translations.
#
# Obok gives you this power over your ebooks - no longer
# are you restricted to one device. This allows you to
# embed foreign fonts into your books, as older Kobo's
# can't display them properly. You can read your books
# on your phones, in different PC readers, and different
# ereader devices. You can share them with your friends
# too, if you like - you can do that with a real book
# after all.
#
"""Manage all Kobo books, either encrypted or DRM-free."""
__version__ = '3.1.1'
import sys
import os
import subprocess
import sqlite3
import base64
import binascii
import re
import zipfile
import hashlib
import xml.etree.ElementTree as ET
import string
import shutil
class ENCRYPTIONError(Exception):
pass
def _load_crypto_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, cast
from ctypes.util import find_library
if sys.platform.startswith('win'):
libcrypto = find_library('libeay32')
else:
libcrypto = find_library('crypto')
if libcrypto is None:
raise ENCRYPTIONError('libcrypto not found')
libcrypto = CDLL(libcrypto)
AES_MAXNR = 14
c_char_pp = POINTER(c_char_p)
c_int_p = POINTER(c_int)
class AES_KEY(Structure):
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
('rounds', c_int)]
AES_KEY_p = POINTER(AES_KEY)
def F(restype, name, argtypes):
func = getattr(libcrypto, name)
func.restype = restype
func.argtypes = argtypes
return func
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
[c_char_p, c_int, AES_KEY_p])
AES_ecb_encrypt = F(None, 'AES_ecb_encrypt',
[c_char_p, c_char_p, AES_KEY_p, c_int])
class AES(object):
def __init__(self, userkey):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise ENCRYPTIONError(_('AES improper key used'))
return
key = self._key = AES_KEY()
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
if rv < 0:
raise ENCRYPTIONError(_('Failed to initialize AES key'))
def decrypt(self, data):
clear = ''
for i in range(0, len(data), 16):
out = create_string_buffer(16)
rv = AES_ecb_encrypt(data[i:i+16], out, self._key, 0)
if rv == 0:
raise ENCRYPTIONError(_('AES decryption failed'))
clear += out.raw
return clear
return AES
def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES
class AES(object):
def __init__(self, key):
self._aes = _AES.new(key, _AES.MODE_ECB)
def decrypt(self, data):
return self._aes.decrypt(data)
return AES
def _load_crypto():
AES = None
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
for loader in cryptolist:
try:
AES = loader()
break
except (ImportError, ENCRYPTIONError):
pass
return AES
AES = _load_crypto()
class KoboLibrary(object):
"""The Kobo library.
This class represents all the information available from the data
written by the Kobo Desktop Edition application, including the list
of books, their titles, and the user's encryption key(s)."""
def __init__ (self):
print u"Obok v{0}\nCopyright © 2012-2014 Physisticated et al.".format(__version__)
if sys.platform.startswith('win'):
if sys.getwindowsversion().major > 5:
self.kobodir = os.environ['LOCALAPPDATA']
else:
self.kobodir = os.path.join(os.environ['USERPROFILE'], 'Local Settings', 'Application Data')
self.kobodir = os.path.join(self.kobodir, 'Kobo', 'Kobo Desktop Edition')
elif sys.platform.startswith('darwin'):
self.kobodir = os.path.join(os.environ['HOME'], 'Library', 'Application Support', 'Kobo', 'Kobo Desktop Edition')
self.bookdir = os.path.join(self.kobodir, 'kepub')
kobodb = os.path.join(self.kobodir, 'Kobo.sqlite')
self.__sqlite = sqlite3.connect(kobodb)
self.__cursor = self.__sqlite.cursor()
self._userkeys = []
self._books = []
self._volumeID = []
def close (self):
"""Closes the database used by the library."""
self.__cursor.close()
self.__sqlite.close()
@property
def userkeys (self):
"""The list of potential userkeys being used by this library.
Only one of these will be valid.
"""
if len(self._userkeys) != 0:
return self._userkeys
userid = self.__getuserid()
for macaddr in self.__getmacaddrs():
self._userkeys.append(self.__getuserkey(macaddr, userid))
return self._userkeys
@property
def books (self):
"""The list of KoboBook objects in the library."""
if len(self._books) != 0:
return self._books
"""Drm-ed kepub"""
for row in self.__cursor.execute('SELECT DISTINCT volumeid, Title, Attribution, Series FROM content_keys, content WHERE contentid = volumeid'):
self._books.append(KoboBook(row[0], row[1], self.__bookfile(row[0]), 'kepub', self.__cursor, author=row[2], series=row[3]))
self._volumeID.append(row[0])
"""Drm-free"""
for f in os.listdir(self.bookdir):
if(f not in self._volumeID):
row = self.__cursor.execute("SELECT Title, Attribution, Series FROM content WHERE ContentID = '" + f + "'").fetchone()
if row is not None:
fTitle = row[0]
self._books.append(KoboBook(f, fTitle, self.__bookfile(f), 'drm-free', self.__cursor, author=row[1], series=row[2]))
self._volumeID.append(f)
"""Sort"""
self._books.sort(key=lambda x: x.title)
return self._books
def __bookfile (self, volumeid):
"""The filename needed to open a given book."""
return os.path.join(self.kobodir, 'kepub', volumeid)
def __getmacaddrs (self):
"""The list of all MAC addresses on this machine."""
macaddrs = []
if sys.platform.startswith('win'):
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
for line in os.popen('ipconfig /all'):
m = c.search(line)
if m:
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
elif sys.platform.startswith('darwin'):
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
matches = c.findall(output)
for m in matches:
# print "m:",m[0]
macaddrs.append(m[0].upper())
return macaddrs
def __getuserid (self):
return self.__cursor.execute('SELECT UserID FROM user WHERE HasMadePurchase = "true"').fetchone()[0]
def __getuserkey (self, macaddr, userid):
deviceid = hashlib.sha256('NoCanLook' + macaddr).hexdigest()
userkey = hashlib.sha256(deviceid + userid).hexdigest()
return binascii.a2b_hex(userkey[32:])
class KoboBook(object):
"""A Kobo book.
A Kobo book contains a number of unencrypted and encrypted files.
This class provides a list of the encrypted files.
Each book has the following instance variables:
volumeid - a UUID which uniquely refers to the book in this library.
title - the human-readable book title.
filename - the complete path and filename of the book.
type - either kepub or drm-free"""
def __init__ (self, volumeid, title, filename, type, cursor, author=None, series=None):
self.volumeid = volumeid
self.title = title
self.author = author
self.series = series
self.series_index = None
self.filename = filename
self.type = type
self.__cursor = cursor
self._encryptedfiles = {}
@property
def encryptedfiles (self):
"""A dictionary of KoboFiles inside the book.
The dictionary keys are the relative pathnames, which are
the same as the pathnames inside the book 'zip' file."""
if (self.type == 'drm-free'):
return self._encryptedfiles
if len(self._encryptedfiles) != 0:
return self._encryptedfiles
# Read the list of encrypted files from the DB
for row in self.__cursor.execute('SELECT elementid,elementkey FROM content_keys,content WHERE volumeid = ? AND volumeid = contentid',(self.volumeid,)):
self._encryptedfiles[row[0]] = KoboFile(row[0], None, base64.b64decode(row[1]))
# Read the list of files from the kepub OPF manifest so that
# we can get their proper MIME type.
# NOTE: this requires that the OPF file is unencrypted!
zin = zipfile.ZipFile(self.filename, "r")
xmlns = {
'ocf': 'urn:oasis:names:tc:opendocument:xmlns:container',
'opf': 'http://www.idpf.org/2007/opf'
}
ocf = ET.fromstring(zin.read('META-INF/container.xml'))
opffile = ocf.find('.//ocf:rootfile', xmlns).attrib['full-path']
basedir = re.sub('[^/]+$', '', opffile)
opf = ET.fromstring(zin.read(opffile))
zin.close()
c = re.compile('/')
for item in opf.findall('.//opf:item', xmlns):
mimetype = item.attrib['media-type']
# Convert relative URIs
href = item.attrib['href']
if not c.match(href):
href = string.join((basedir, href), '')
# Update books we've found from the DB.
if href in self._encryptedfiles:
self._encryptedfiles[href].mimetype = mimetype
return self._encryptedfiles
@property
def has_drm (self):
return not self.type == 'drm-free'
class KoboFile(object):
"""An encrypted file in a KoboBook.
Each file has the following instance variables:
filename - the relative pathname inside the book zip file.
mimetype - the file's MIME type, e.g. 'image/jpeg'
key - the encrypted page key."""
def __init__ (self, filename, mimetype, key):
self.filename = filename
self.mimetype = mimetype
self.key = key
def decrypt (self, userkey, contents):
"""
Decrypt the contents using the provided user key and the
file page key. The caller must determine if the decrypted
data is correct."""
# The userkey decrypts the page key (self.key)
keyenc = AES(userkey)
decryptedkey = keyenc.decrypt(self.key)
# The decrypted page key decrypts the content
pageenc = AES(decryptedkey)
return self.__removeaespadding(pageenc.decrypt(contents))
def check (self, contents):
"""
If the contents uses some known MIME types, check if it
conforms to the type. Throw a ValueError exception if not.
If the contents uses an uncheckable MIME type, don't check
it and don't throw an exception.
Returns True if the content was checked, False if it was not
checked."""
if self.mimetype == 'application/xhtml+xml':
if contents[:5]=="<?xml":
return True
else:
print "Bad XML: ",contents[:5]
raise ValueError
if self.mimetype == 'image/jpeg':
if contents[:3] == '\xff\xd8\xff':
return True
else:
print "Bad JPEG: ", contents[:3].encode('hex')
raise ValueError()
return False
def __removeaespadding (self, contents):
"""
Remove the trailing padding, using what appears to be the CMS
algorithm from RFC 5652 6.3"""
lastchar = binascii.b2a_hex(contents[-1:])
strlen = int(lastchar, 16)
padding = strlen
if strlen == 1:
return contents[:-1]
if strlen < 16:
for i in range(strlen):
testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)])
if testchar != lastchar:
padding = 0
if padding > 0:
contents = contents[:-padding]
return contents
if __name__ == '__main__':
lib = KoboLibrary()
for i, book in enumerate(lib.books):
print ('%d: %s' % (i + 1, book.title)).encode('ascii', 'ignore')
num_string = raw_input("Convert book number... ")
try:
num = int(num_string)
book = lib.books[num - 1]
except (ValueError, IndexError):
exit()
print "Converting", book.title
zin = zipfile.ZipFile(book.filename, "r")
# make filename out of Unicode alphanumeric and whitespace equivalents from title
outname = "%s.epub" % (re.sub('[^\s\w]', '', book.title, 0, re.UNICODE))
if (book.type == 'drm-free'):
print "DRM-free book, conversion is not needed"
shutil.copyfile(book.filename, outname)
print "Book saved as", os.path.join(os.getcwd(), outname)
exit(0)
result = 1
for userkey in lib.userkeys:
# print "Trying key: ",userkey.encode('hex_codec')
confirmedGood = False
try:
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
for filename in zin.namelist():
contents = zin.read(filename)
if filename in book.encryptedfiles:
file = book.encryptedfiles[filename]
contents = file.decrypt(userkey, contents)
# Parse failures mean the key is probably wrong.
if not confirmedGood:
confirmedGood = file.check(contents)
zout.writestr(filename, contents)
zout.close()
print "Book saved as", os.path.join(os.getcwd(), outname)
result = 0
break
except ValueError:
print "Decryption failed, trying next key"
zout.close()
os.remove(outname)
zin.close()
lib.close()
exit(result)

View File

@@ -1,231 +0,0 @@
#!/usr/bin/env python
#
# Updated September 2013 by Anon
# Version 2.01
# Incorporated minor fixes posted at Apprentice Alf's.
#
# Updates July 2012 by Michael Newton
# PWSD ID is no longer a MAC address, but should always
# be stored in the registry. Script now works with OS X
# and checks plist for values instead of registry. Must
# have biplist installed for OS X support.
#
##########################################################
# KOBO DRM CRACK BY #
# PHYSISTICATED #
##########################################################
# This app was made for Python 2.7 on Windows 32-bit
#
# This app needs pycrypto - get from here:
# http://www.voidspace.org.uk/python/modules.shtml
#
# Usage: obok.py
# Choose the book you want to decrypt
#
# Shouts to my krew - you know who you are - and one in
# particular who gave me a lot of help with this - thank
# you so much!
#
# Kopimi /K\
# Keep sharing, keep copying, but remember that nothing is
# for free - make sure you compensate your favorite
# authors - and cut out the middle man whenever possible
# ;) ;) ;)
#
# DRM AUTOPSY
# The Kobo DRM was incredibly easy to crack, but it took
# me months to get around to making this. Here's the
# basics of how it works:
# 1: Get MAC address of first NIC in ipconfig (sometimes
# stored in registry as pwsdid)
# 2: Get user ID (stored in tons of places, this gets it
# from HKEY_CURRENT_USER\Software\Kobo\Kobo Desktop
# Edition\Browser\cookies)
# 3: Concatenate and SHA256, take the second half - this
# is your master key
# 4: Open %LOCALAPPDATA%\Kobo Desktop Editions\Kobo.sqlite
# and dump content_keys
# 5: Unbase64 the keys, then decode these with the master
# key - these are your page keys
# 6: Unzip EPUB of your choice, decrypt each page with its
# page key, then zip back up again
#
# WHY USE THIS WHEN INEPT WORKS FINE? (adobe DRM stripper)
# Inept works very well, but authors on Kobo can choose
# what DRM they want to use - and some have chosen not to
# let people download them with Adobe Digital Editions -
# they would rather lock you into a single platform.
#
# With Obok, you can sync Kobo Desktop, decrypt all your
# ebooks, and then use them on whatever device you want
# - you bought them, you own them, you can do what you
# like with them.
#
# Obok is Kobo backwards, but it is also means "next to"
# in Polish.
# When you buy a real book, it is right next to you. You
# can read it at home, at work, on a train, you can lend
# it to a friend, you can scribble on it, and add your own
# explanations/translations.
#
# Obok gives you this power over your ebooks - no longer
# are you restricted to one device. This allows you to
# embed foreign fonts into your books, as older Kobo's
# can't display them properly. You can read your books
# on your phones, in different PC readers, and different
# ereader devices. You can share them with your friends
# too, if you like - you can do that with a real book
# after all.
#
"""
Decrypt Kobo encrypted EPUB books.
"""
import os
import sys
if sys.platform.startswith('win'):
import _winreg
elif sys.platform.startswith('darwin'):
from biplist import readPlist
import re
import string
import hashlib
import sqlite3
import base64
import binascii
import zipfile
from Crypto.Cipher import AES
def SHA256(raw):
return hashlib.sha256(raw).hexdigest()
def RemoveAESPadding(contents):
lastchar = binascii.b2a_hex(contents[-1:])
strlen = int(lastchar, 16)
padding = strlen
if(strlen == 1):
return contents[:-1]
if(strlen < 16):
for i in range(strlen):
testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)])
if(testchar != lastchar):
padding = 0
if(padding > 0):
contents = contents[:-padding]
return contents
def GetVolumeKeys(dbase, enc):
volumekeys = {}
for row in dbase.execute("SELECT * from content_keys"):
if(row[0] not in volumekeys):
volumekeys[row[0]] = {}
volumekeys[row[0]][row[1]] = {}
volumekeys[row[0]][row[1]]["encryptedkey"] = base64.b64decode(row[2])
volumekeys[row[0]][row[1]]["decryptedkey"] = enc.decrypt(volumekeys[row[0]][row[1]]["encryptedkey"])
# get book name
for key in volumekeys.keys():
volumekeys[key]["title"] = dbase.execute("SELECT Title from content where ContentID = '%s'" % (key)).fetchone()[0]
return volumekeys
def ByteArrayToString(bytearr):
wincheck = re.match("@ByteArray\\((.+)\\)", bytearr)
if wincheck:
return wincheck.group(1)
return bytearr
def GetUserHexKey(prefs = ""):
"find wsuid and pwsdid"
wsuid = ""
pwsdid = ""
if sys.platform.startswith('win'):
regkey_browser = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Software\\Kobo\\Kobo Desktop Edition\\Browser")
cookies = _winreg.QueryValueEx(regkey_browser, "cookies")
bytearrays = cookies[0]
elif sys.platform.startswith('darwin'):
cookies = readPlist(prefs)
bytearrays = cookies["Browser.cookies"]
for bytearr in bytearrays:
cookie = ByteArrayToString(bytearr)
print cookie
wsuidcheck = re.match("^wsuid=([0-9a-f-]+)", cookie)
if(wsuidcheck):
wsuid = wsuidcheck.group(1)
pwsdidcheck = re.match("^pwsdid=([0-9a-f-]+)", cookie)
if (pwsdidcheck):
pwsdid = pwsdidcheck.group(1)
if(wsuid == "" or pwsdid == ""):
print "wsuid or pwsdid key not found :/"
exit()
preuserkey = string.join((pwsdid, wsuid), "")
print SHA256(pwsdid)
userkey = SHA256(preuserkey)
return userkey[32:]
# get dirs
if sys.platform.startswith('win'):
delim = "\\"
if (sys.getwindowsversion().major > 5):
kobodir = string.join((os.environ['LOCALAPPDATA'], "Kobo\\Kobo Desktop Edition"), delim)
else:
kobodir = string.join((os.environ['USERPROFILE'], "Local Settings\\Application Data\\Kobo\\Kobo Desktop Edition"), delim)
prefs = ""
elif sys.platform.startswith('darwin'):
delim = "/"
kobodir = string.join((os.environ['HOME'], "Library/Application Support/Kobo/Kobo Desktop Edition"), delim)
prefs = string.join((os.environ['HOME'], "Library/Preferences/com.kobo.Kobo Desktop Edition.plist"), delim)
sqlitefile = string.join((kobodir, "Kobo.sqlite"), delim)
bookdir = string.join((kobodir, "kepub"), delim)
# get key
userkeyhex = GetUserHexKey(prefs)
# load into AES
userkey = binascii.a2b_hex(userkeyhex)
enc = AES.new(userkey, AES.MODE_ECB)
# open sqlite
conn = sqlite3.connect(sqlitefile)
dbcursor = conn.cursor()
# get volume keys
volumekeys = GetVolumeKeys(dbcursor, enc)
# choose a volumeID
volumeid = ""
print "Choose a book to decrypt:"
i = 1
for key in volumekeys.keys():
print "%d: %s" % (i, volumekeys[key]["title"])
i += 1
num = input("...")
i = 1
for key in volumekeys.keys():
if(i == num):
volumeid = key
i += 1
if(volumeid == ""):
exit()
zippath = string.join((bookdir, volumeid), delim)
z = zipfile.ZipFile(zippath, "r")
# make filename out of Unicode alphanumeric and whitespace equivalents from title
outname = "%s.epub" % (re.sub("[^\s\w]", "", volumekeys[volumeid]["title"], 0, re.UNICODE))
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
for filename in z.namelist():
#print filename
# read in and decrypt
if(filename in volumekeys[volumeid]):
# do decrypted version
pagekey = volumekeys[volumeid][filename]["decryptedkey"]
penc = AES.new(pagekey, AES.MODE_ECB)
contents = RemoveAESPadding(penc.decrypt(z.read(filename)))
# need to fix padding
zout.writestr(filename, contents)
else:
zout.writestr(filename, z.read(filename))
print "Book saved as %s%s%s" % (os.getcwd(), delim, outname)