summaryrefslogtreecommitdiffstats
path: root/Obok_plugin/obok/obok.py
blob: dc0a30d79e4f1761cb87b947f0b75155daee7526 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Version 10.0.3 July 2022
# Fix Calibre 6
#
# Version 10.0.1 February 2022
# Remove OpenSSL support to only support PyCryptodome; clean up the code.
#
# Version 10.0.0 November 2021
# Merge https://github.com/apprenticeharper/DeDRM_tools/pull/1691 to fix
# key fetch issues on some machines.
#
# Version 4.1.0 February 2021
# Add detection for Kobo directory location on Linux
#
# Version 4.0.0 September 2020
# Python 3.0
#
# Version 3.2.5 December 2016
# Improve detection of good text decryption.
#
# Version 3.2.4 December 2016
# Remove incorrect support for Kobo Desktop under Wine
#
# Version 3.2.3 October 2016
# Fix for windows network user and more xml fixes
#
# Version 3.2.2 October 2016
# Change to the way the new database version is handled.
#
# Version 3.2.1 September 2016
# Update for v4.0 of Windows Desktop app.
#
# Version 3.2.0 January 2016
# Update for latest version of Windows Desktop app.
# Support Kobo devices in the command line version.
#
# Version 3.1.9 November 2015
# Handle Kobo Desktop under wine on Linux
#
# Version 3.1.8 November 2015
# Handle the case of Kobo Arc or Vox device (i.e. don't crash).
#
# Version 3.1.7 October 2015
# Handle the case of no device or database more gracefully.
#
# Version 3.1.6 September 2015
# Enable support for Kobo devices
# More character encoding fixes (unicode strings)
#
# Version 3.1.5 September 2015
# Removed requirement that a purchase has been made.
# Also add in character encoding fixes
#
# Version 3.1.4 September 2015
# Updated for version 3.17 of the Windows Desktop app.
#
# Version 3.1.3 August 2015
# Add translations for Portuguese and Arabic
#
# 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."""
from __future__ import print_function

__version__ = '10.0.1'
__about__ =  "Obok v{0}\nCopyright © 2012-2022 Physisticated et al.".format(__version__)

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
import argparse
import tempfile

try:
    from Cryptodome.Cipher import AES
except ImportError:
    from Crypto.Cipher import AES

def unpad(data, padding=16):
    if sys.version_info[0] == 2:
        pad_len = ord(data[-1])
    else:
        pad_len = data[-1]

    return data[:-pad_len]


can_parse_xml = True
try:
  from xml.etree import ElementTree as ET
  # print "using xml.etree for xml parsing"
except ImportError:
  can_parse_xml = False
  # print "Cannot find xml.etree, disabling extraction of serial numbers"

# List of all known hash keys
KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']

class ENCRYPTIONError(Exception):
    pass

# 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,str) or isinstance(data,unicode):
            # str for Python3, unicode for Python2
            data = data.encode(self.encoding,"replace")
        try:
            buffer = getattr(self.stream, 'buffer', self.stream)
            # self.stream.buffer for Python3, self.stream for Python2
            buffer.write(data)
            buffer.flush()
        except:
            # We can do nothing if a write fails
            raise
    def __getattr__(self, attr):
        return getattr(self.stream, attr)


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, serials = [], device_path = None, desktopkobodir = u""):
        print(__about__)
        self.kobodir = u""
        kobodb = u""

        # Order of checks
        # 1. first check if a device_path has been passed in, and whether
        #    we can find the sqlite db in the respective place
        # 2. if 1., and we got some serials passed in (from saved
        #    settings in calibre), just use it
        # 3. if 1. worked, but we didn't get serials, try to parse them
        #    from the device, if this didn't work, unset everything
        # 4. if by now we don't have kobodir set, give up on device and
        #    try to use the Desktop app.

        # step 1. check whether this looks like a real device
        if (device_path):
            # we got a device path
            self.kobodir = os.path.join(device_path, ".kobo")
            # devices use KoboReader.sqlite
            kobodb  = os.path.join(self.kobodir, "KoboReader.sqlite")
            if (not(os.path.isfile(kobodb))):
                # device path seems to be wrong, unset it
                device_path = u""
                self.kobodir = u""
                kobodb  = u""

        if (self.kobodir):
            # step 3. we found a device but didn't get serials, try to get them
            if (len(serials) == 0):
                # we got a device path but no saved serial
                # try to get the serial from the device
                # print "get_device_settings - device_path = {0}".format(device_path)
                # get serial from device_path/.adobe-digital-editions/device.xml
                if can_parse_xml:
                    devicexml = os.path.join(device_path, '.adobe-digital-editions', 'device.xml')
                    # print "trying to load {0}".format(devicexml)
                    if (os.path.exists(devicexml)):
                        # print "trying to parse {0}".format(devicexml)
                        xmltree = ET.parse(devicexml)
                        for node in xmltree.iter():
                            if "deviceSerial" in node.tag:
                                serial = node.text
                                # print "found serial {0}".format(serial)
                                serials.append(serial)
                                break
                    else:
                        # print "cannot get serials from device."
                        device_path = u""
                        self.kobodir = u""
                        kobodb  = u""

        if (self.kobodir == u""):
            # step 4. we haven't found a device with serials, so try desktop apps
            if desktopkobodir != u'':
                self.kobodir = desktopkobodir

            if (self.kobodir == u""):
                if sys.platform.startswith('win'):
                    try:
                        import winreg
                    except ImportError:
                        import _winreg as winreg
                    if sys.getwindowsversion().major > 5:
                        if 'LOCALAPPDATA' in os.environ.keys():
                            # Python 2.x does not return unicode env. Use Python 3.x
                            if sys.version_info[0] == 2:
                                self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
                            else: 
                                self.kobodir = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
                    if (self.kobodir == u""):
                        if 'USERPROFILE' in os.environ.keys():
                            # Python 2.x does not return unicode env. Use Python 3.x
                            if sys.version_info[0] == 2:
                                self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), "Local Settings", "Application Data")
                            else: 
                                self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings("%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")
                elif sys.platform.startswith('linux'):

                    #sets ~/.config/calibre as the location to store the kobodir location info file and creates this directory if necessary
                    kobodir_cache_dir = os.path.join(os.environ['HOME'], ".config", "calibre")
                    if not os.path.isdir(kobodir_cache_dir):
                        os.mkdir(kobodir_cache_dir)
                    
                    #appends the name of the file we're storing the kobodir location info to the above path
                    kobodir_cache_file = str(kobodir_cache_dir) + "/" + "kobo location"
                    
                    """if the above file does not exist, recursively searches from the root
                    of the filesystem until kobodir is found and stores the location of kobodir
                    in that file so this loop can be skipped in the future"""
                    original_stdout = sys.stdout
                    if not os.path.isfile(kobodir_cache_file):
                        for root, dirs, files in os.walk('/'):
                            for file in files:
                                if file == 'Kobo.sqlite':
                                    kobo_linux_path = str(root)
                                    with open(kobodir_cache_file, 'w') as f:
                                        sys.stdout = f
                                        print(kobo_linux_path, end='')
                                        sys.stdout = original_stdout

                    f = open(kobodir_cache_file, 'r' )
                    self.kobodir = f.read()

            # desktop versions use Kobo.sqlite
            kobodb = os.path.join(self.kobodir, "Kobo.sqlite")
            # check for existence of file
            if (not(os.path.isfile(kobodb))):
                # give up here, we haven't found anything useful
                self.kobodir = u""
                kobodb  = u""

        if (self.kobodir != u""):
            self.bookdir = os.path.join(self.kobodir, "kepub")
            # make a copy of the database in a temporary file
            # so we can ensure it's not using WAL logging which sqlite3 can't do.
            self.newdb = tempfile.NamedTemporaryFile(mode='wb', delete=False)
            print(self.newdb.name)
            olddb = open(kobodb, 'rb')
            self.newdb.write(olddb.read(18))
            self.newdb.write(b'\x01\x01')
            olddb.read(2)
            self.newdb.write(olddb.read())
            olddb.close()
            self.newdb.close()
            self.__sqlite = sqlite3.connect(self.newdb.name)
            self.__sqlite.text_factory = lambda b: b.decode("utf-8", errors="ignore")
            self.__cursor = self.__sqlite.cursor()
            self._userkeys = []
            self._books = []
            self._volumeID = []
            self._serials = serials

    def close (self):
        """Closes the database used by the library."""
        self.__cursor.close()
        self.__sqlite.close()
        # delete the temporary copy of the database
        os.remove(self.newdb.name)

    @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
        for macaddr in self.__getmacaddrs():
            self._userkeys.extend(self.__getuserkeys(macaddr))
        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)
            try: 
                output = subprocess.Popen('ipconfig /all', shell=True, stdout=subprocess.PIPE, text=True).stdout
                for line in output:
                    m = c.search(line)
                    if m:
                        macaddrs.append(re.sub("-", ":", m.group(1)).upper())
            except:
                output = subprocess.Popen('wmic nic where PhysicalAdapter=True get MACAddress', shell=True, stdout=subprocess.PIPE, text=True).stdout
                for line in output:
                    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, encoding='utf-8')
            matches = c.findall(output)
            for m in matches:
                # print "m:{0}".format(m[0])
                macaddrs.append(m[0].upper())
        else:
            # probably linux

            # let's try ip
            c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
            for line in os.popen('ip -br link'):
                m = c.search(line)
                if m:
                    macaddrs.append(m.group(1).upper())

            # let's try ipconfig under wine
            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())

        # extend the list of macaddrs in any case with the serials
        # cannot hurt ;-)
        macaddrs.extend(self._serials)

        return macaddrs

    def __getuserids (self):
        userids = []
        cursor = self.__cursor.execute('SELECT UserID FROM user')
        row = cursor.fetchone()
        while row is not None:
            try:
                userid = row[0]
                userids.append(userid)
            except:
                pass
            row = cursor.fetchone()
        return userids

    def __getuserkeys (self, macaddr):
        userids = self.__getuserids()
        userkeys = []
        for hash in KOBO_HASH_KEYS:
            deviceid = hashlib.sha256((hash + macaddr).encode('ascii')).hexdigest()
            for userid in userids:
                userkey = hashlib.sha256((deviceid + userid).encode('ascii')).hexdigest()
                userkeys.append(binascii.a2b_hex(userkey[32:]))
        return userkeys

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 = ''.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)
        decryptedkey = AES.new(userkey, AES.MODE_ECB).decrypt(self.key)
        # The decrypted page key decrypts the content. Padding is PKCS#7
        return unpad(AES.new(decryptedkey, AES.MODE_ECB).decrypt(contents), 16)

    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':
            # assume utf-8 with no BOM
            textoffset = 0
            stride = 1
            print("Checking text:{0}:".format(contents[:10]))
            # check for byte order mark
            if contents[:3]==b"\xef\xbb\xbf":
                # seems to be utf-8 with BOM
                print("Could be utf-8 with BOM")
                textoffset = 3
            elif contents[:2]==b"\xfe\xff":
                # seems to be utf-16BE
                print("Could be  utf-16BE")
                textoffset = 3
                stride = 2
            elif contents[:2]==b"\xff\xfe":
                # seems to be utf-16LE
                print("Could be  utf-16LE")
                textoffset = 2
                stride = 2
            else:
                print("Perhaps utf-8 without BOM")

            # now check that the first few characters are in the ASCII range
            for i in range(textoffset,textoffset+5*stride,stride):
                if contents[i]<32 or contents[i]>127:
                    # Non-ascii, so decryption probably failed
                    print("Bad character at {0}, value {1}".format(i,contents[i]))
                    raise ValueError
            print("Seems to be good text")
            return True
            if contents[:5]==b"<?xml" or contents[:8]==b"\xef\xbb\xbf<?xml":
                # utf-8
                return True
            elif contents[:14]==b"\xfe\xff\x00<\x00?\x00x\x00m\x00l":
                # utf-16BE
                return True
            elif contents[:14]==b"\xff\xfe<\x00?\x00x\x00m\x00l\x00":
                # utf-16LE
                return True
            elif contents[:9]==b"<!DOCTYPE" or contents[:12]==b"\xef\xbb\xbf<!DOCTYPE":
                # utf-8 of weird <!DOCTYPE start
                return True
            elif contents[:22]==b"\xfe\xff\x00<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E":
                # utf-16BE of weird <!DOCTYPE start
                return True
            elif contents[:22]==b"\xff\xfe<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E\x00":
                # utf-16LE of weird <!DOCTYPE start
                return True
            else:
                print("Bad XML: {0}".format(contents[:8]))
                raise ValueError
        elif self.mimetype == 'image/jpeg':
            if contents[:3] == b'\xff\xd8\xff':
                return True
            else:
                print("Bad JPEG: {0}".format(contents[:3].hex()))
                raise ValueError()
        return False


def decrypt_book(book, lib):
    print("Converting {0}".format(book.title))
    zin = zipfile.ZipFile(book.filename, "r")
    # make filename out of Unicode alphanumeric and whitespace equivalents from title
    outname = "{0}.epub".format(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 {0}".format(os.path.join(os.getcwd(), outname)))
        return 0
    result = 1
    for userkey in lib.userkeys:
        print("Trying key: {0}".format(userkey.hex()))
        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.
                    file.check(contents)
                zout.writestr(filename, contents)
            zout.close()
            print("Decryption succeeded.")
            print("Book saved as {0}".format(os.path.join(os.getcwd(), outname)))
            result = 0
            break
        except ValueError:
            print("Decryption failed.")
            zout.close()
            os.remove(outname)
    zin.close()
    return result


def cli_main():
    description = __about__
    epilog = "Parsing of arguments failed."
    parser = argparse.ArgumentParser(prog=sys.argv[0], description=description, epilog=epilog)
    parser.add_argument('--devicedir', default='/media/KOBOeReader', help="directory of connected Kobo device")
    parser.add_argument('--all', action='store_true', help="flag for converting all books on device")
    args = vars(parser.parse_args())
    serials = []
    devicedir = u""
    if args['devicedir']:
        devicedir = args['devicedir']

    lib = KoboLibrary(serials, devicedir)

    if args['all']:
        books = lib.books
    else:
        for i, book in enumerate(lib.books):
            print("{0}: {1}".format(i + 1, book.title))
        print("Or 'all'")

        choice = input("Convert book number... ")
        if choice == "all":
            books = list(lib.books)
        else:
            try:
                num = int(choice)
                books = [lib.books[num - 1]]
            except (ValueError, IndexError):
                print("Invalid choice. Exiting...")
                sys.exit()

    results = [decrypt_book(book, lib) for book in books]
    lib.close()
    overall_result = all(result != 0 for result in results)
    if overall_result != 0:
        print("Could not decrypt book with any of the keys found.")
    return overall_result


if __name__ == '__main__':
    sys.stdout=SafeUnbuffered(sys.stdout)
    sys.stderr=SafeUnbuffered(sys.stderr)
    sys.exit(cli_main())