Compare commits

...

62 Commits

Author SHA1 Message Date
Apprentice Harper
73af5d355d whitespace and some unicode/bytes
Minor changes.
2021-04-11 16:43:16 +01:00
Apprentice Harper
45a1a64db5 Update version and FAQs
Version 7.2.0 with all the latest pull requests, including on for the latest KFX encryption.
2021-04-11 15:28:33 +01:00
Apprentice Harper
bc1c3c2197 Merge pull request #1490 from llrosy798/patch-1
update voucher envelope obfuscation table
2021-04-11 15:14:08 +01:00
Apprentice Harper
79cfddfbee Merge pull request #1650 from romanbsd/bugfix
Python 3.x fix
2021-04-11 15:10:32 +01:00
Apprentice Harper
aa41bba68c Merge pull request #1615 from ableeker/python3
Python 3 fix
2021-04-11 15:09:39 +01:00
Apprentice Harper
86a90117e5 Merge pull request #1586 from raiden64/master
Fix in keyfetch for obok on MacOS
2021-04-11 15:07:17 +01:00
Apprentice Harper
874a6b8de9 Merge pull request #1575 from journeyman88/master
Fix in keyfetch for obok on win10
2021-04-11 14:05:09 +01:00
Apprentice Harper
01c654cb68 Merge pull request #1560 from Threak/master
Try new openssl library name
2021-04-11 14:04:04 +01:00
Apprentice Harper
5bc28623cb Merge pull request #1546 from mkb79/master
Enhance parsing DrmIon files
2021-04-11 14:00:21 +01:00
Apprentice Harper
c1d7fcbb7f Merge pull request #1545 from lejando/patch-1
Update FAQs.md. Thanks, lejando.
2021-04-11 13:57:29 +01:00
Apprentice Harper
45eefd6c80 Merge pull request #1539 from josdion/master
Preserve filename encoding flag when fixing epub archive
2021-04-11 13:56:19 +01:00
Roman Shterenzon
33e37eb375 Python 3.x fix 2021-04-08 16:46:14 +03:00
Aldo Bleeker
4229b8ff85 Another Python 3 fix 2021-04-05 17:06:24 +02:00
Aldo Bleeker
91e4645315 Another Python 3 fix 2021-04-05 12:16:02 +02:00
Aldo Bleeker
425d8af73e Python 3 fix 2021-03-22 19:24:34 +01:00
raiden64
0ce86fa8db Fix in keyfetch for obok on MacOS 2021-03-05 22:54:53 +01:00
journeyman88
ecc7db09a9 Fix in keyfetch for obok on win10
According to calibre debug the ipconfig command returned some invalid utf-8 characters (I think is maybe an issue due to the Python2 switch-off as the 4.x version worked fine).
To solve this I've changed the external call and modified the regex to match both the output of "ipconfig" and that of "wmic".
2021-03-01 21:15:20 +01:00
Threak
d7ddc2ab93 Try new openssl library name 2021-02-26 18:50:10 +01:00
mkb79
fd51422a36 Enhance parsing DrmIon files
Adding support for parsing plaintext in DrmIon files.

This is needed by my kindle project. When downloading an ebook with my package it gives me a metadata file wich is DrmIon encoded. This file containes plaintext instead of encrypted pages.
2021-02-22 14:16:15 +01:00
lejando
cb36ca1b0d Update FAQs.md
Removed space from Mac and Win and period from Mac SHA-256 Hashes, which prevent automatic comparison.
2021-02-22 08:51:00 +01:00
Apprentice Harper
76a47e0dd0 Version number update
Update to 7.1.0 for a full release
2021-02-21 14:35:49 +00:00
Apprentice Harper
70a754fb46 Merge pull request #1529 from ableeker/python3
Fix for Python 3
2021-02-21 14:19:59 +00:00
josdion
ffd79d5fe4 Preserve filename encoding flag when fixing epub archive 2021-02-18 12:38:19 +02:00
Aldo Bleeker
21a7b13524 Fix for Python 3 2021-02-14 12:50:55 +01:00
Apprentice Harper
52bdbe95c9 Merge pull request #1522 from lkcv/patch-1
Add detection for Kobo directory location on Linux
2021-02-14 08:56:58 +00:00
Apprentice Harper
495dda3809 Merge pull request #1502 from ableeker/python3
Fix for broken book keys
2021-02-14 08:55:56 +00:00
Apprentice Harper
52e83922c0 Merge pull request #1499 from xxyzz/kfx
encode serialnum before returning it, close #1479
2021-02-14 08:50:26 +00:00
lkcv
6cbc5285cb Update obok.py 2021-02-07 21:21:03 -05:00
Aldo Bleeker
33b9630ca5 Fix for broken book keys 2021-01-28 13:06:59 +01:00
xxyzz
9346f86f73 encode serialnum before returning it, close #1479 2021-01-27 14:31:05 +08:00
Apprentice Harper
8d2d6627cf Merge pull request #1482 from 2weak2live/master
Fix python3 encoding problem in voucher decryption
2021-01-23 14:32:43 +00:00
Apprentice Harper
6f198b247c Merge pull request #1481 from icaroscherma/patch-1
[Tetrachroma FileOpen] Fixes Python 2.7 import issue, not linked to pywin
2021-01-23 14:30:21 +00:00
Apprentice Harper
9fb95eff41 Merge pull request #1491 from jony0008/master
Update sv
2021-01-23 14:29:56 +00:00
llrosy798
0b2b81fd23 fix previous bug 2021-01-21 23:48:04 +09:00
llrosy798
63aecc598f update secret table 2021-01-21 23:46:03 +09:00
llrosy798
51c8be6baf fill unknown symbols in known catalog 2021-01-21 23:41:22 +09:00
Jony
7aab8a3711 Update sv 2021-01-20 12:01:00 +01:00
2Weak2Live
2789cee331 Fix python3 encoding problem in voucher decryption 2021-01-13 22:44:11 -05:00
Ícaro R. Scherma
823704cf36 Fixes Python 2.7 import issue, not linked to pywin 2021-01-13 16:44:16 -08:00
Apprentice Harper
a7974f0f14 Update ineptpdf.py
integer division, and version
2021-01-03 16:11:02 +00:00
Apprentice Harper
ed412bee35 Updated to inept.pdf for PC
Contributed changes for PC compatibility. Thanks, Aldo.

Update main version to 7.0.2
2021-01-03 16:01:14 +00:00
Apprentice Harper
6cee615f26 Update ineptpdf.py
Fix handling of metadata
2021-01-03 15:35:17 +00:00
Apprentice Harper
c4581b4d72 Version to 7.0.1, ineptpdf fixes
ineptpdf should now decrypt at least some Adobe PDFs
2020-12-30 12:14:04 +00:00
Apprentice Harper
f6a568bcc1 Update ineptepub.py
Handle uncompressed elements (if any) in the zip file.
2020-12-27 12:16:11 +00:00
Apprentice Harper
bf6170e613 Merge pull request #1445 from ableeker/python3
Some more fixes for ePub
2020-12-26 16:02:12 +00:00
Apprentice Harper
afcd79c0cc Merge pull request #1443 from jony0008/master
Update sv translation
2020-12-26 16:00:04 +00:00
Apprentice Harper
fdf0389936 MobiDeDRM fixes
Change handling of PIDs to cope with byte arrays or strings passed in. Also fixed handling of a very old default key format.
2020-12-26 15:58:42 +00:00
Aldo Bleeker
5599c1694b Some more fixes for ePub 2020-12-26 15:36:10 +01:00
Jony
dff90fae6f Update sv translation 2020-12-25 12:47:14 +00:00
Apprentice Harper
d33f679eae Merge pull request #1413 from ableeker/python3
Small fix to make Obok help link work.
2020-12-13 11:28:51 +00:00
Aldo Bleeker
225e74a334 Small fix to make Obok help work. 2020-12-09 17:34:24 +01:00
Apprentice Harper
13e9a14907 Merge pull request #1398 from xxyzz/config
return str from load_resource()
2020-12-04 12:52:42 +00:00
Apprentice Harper
92ea0a2f24 Merge pull request #1392 from penenkel/patch-1
Add conversion from bytearray to bytes so that pids are hashable
2020-12-04 12:51:26 +00:00
xxyzz
a1059650f6 return str from load_resource() 2020-12-03 19:02:09 +08:00
penenkel
a3cc221932 Revert changes to k4mobidedrm.py 2020-12-02 22:36:29 +01:00
penenkel
6732be1434 getPidList() now returns pids as bytes instead of bytearrays 2020-12-02 22:34:29 +01:00
penenkel
ad5cb056f0 Add conversion from bytearray to bytes so that pids are hashable 2020-11-30 23:25:01 +01:00
Apprentice Harper
d3c7388327 Merge pull request #1389 from ableeker/python3
Python 3 fixes for __init__.py
2020-11-29 16:35:46 +00:00
Aldo Bleeker
8e436ad920 Python 3 fixes fort correct version of __init__.py 2020-11-29 16:54:45 +01:00
Aldo Bleeker
ae806f734e Python 3 fixes for __init__.py 2020-11-29 13:39:04 +01:00
Apprentice Harper
ccfa454226 Merge branch 'Python2' - the DeDRM plugn version change 2020-11-29 10:47:09 +00:00
Apprentice Harper
6716db1f62 Derive calibre version tuple from __version__ string 2020-11-29 10:40:14 +00:00
22 changed files with 433 additions and 312 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.DS_Store

View File

@@ -5,7 +5,7 @@
# Copyright © 2008-2020 Apprentice Harper et al.
__license__ = 'GPL v3'
__version__ = '7.0.0'
__version__ = '7.2.1'
__docformat__ = 'restructuredtext en'
@@ -69,15 +69,21 @@ __docformat__ = 'restructuredtext en'
# 6.6.3 - More cleanup of kindle book names and start of support for .kinf2018
# 6.7.0 - Handle new library in calibre.
# 6.8.0 - Full support for .kinf2018 and new KFX encryption (Kindle for PC/Mac 2.5+)
# 6.8.1 - Kindle key fix for Mac OS X Big Syr
# 7.0.0 - Switched to Python 3 for calibre 5.0. Thanks to all who comtibuted
# 6.8.1 - Kindle key fix for Mac OS X Big Sur
# 7.0.0 - Switched to Python 3 for calibre 5.0. Thanks to all who contributed
# 7.0.1 - More Python 3 changes. Adobe PDF decryption should now work in some cases
# 7.0.2 - More Python 3 changes. Adobe PDF decryption should now work on PC too.
# 7.0.3 - More Python 3 changes. Integer division in ineptpdf.py
# 7.1.0 - Full release for calibre 5.x
# 7.2.0 - Update for latest KFX changes, and Python 3 Obok fixes.
# 7.2.1 - Whitespace!
"""
Decrypt DRMed ebooks.
"""
PLUGIN_NAME = "DeDRM"
PLUGIN_VERSION_TUPLE = (7, 0, 0)
PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@@ -421,7 +427,7 @@ class DeDRM(FileTypePlugin):
# Attempt to decrypt epub with each encryption key (generated or provided).
print("{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
userkey = userkeyhex.decode('hex')
userkey = codecs.decode(userkeyhex,'hex')
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
of = self.temporary_file(".pdf")
@@ -467,7 +473,7 @@ class DeDRM(FileTypePlugin):
newkeys = []
for keyvalue in defaultkeys:
if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values():
if codecs.encode(keyvalue,'hex') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue)
if len(newkeys) > 0:
@@ -491,7 +497,7 @@ class DeDRM(FileTypePlugin):
# Store the new successful key in the defaults
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
try:
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex'))
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',codecs.encode(keyvalue,'hex'))
dedrmprefs.writeprefs()
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except:
@@ -596,7 +602,7 @@ class DeDRM(FileTypePlugin):
of = self.temporary_file(".pmlz")
# Give the userkey, ebook and TemporaryPersistent file to the decryption function.
result = erdr2pml.decryptBook(path_to_ebook, of.name, True, userkey.decode('hex'))
result = erdr2pml.decryptBook(path_to_ebook, of.name, True, codecs.decode(userkey,'hex'))
of.close()

View File

@@ -115,7 +115,9 @@ if iswindows:
def _load_crypto_libcrypto():
from ctypes.util import find_library
libcrypto = find_library('libeay32')
libcrypto = find_library('libcrypto-1_1')
if libcrypto is None:
libcrypto = find_library('libeay32')
if libcrypto is None:
raise ADEPTError('libcrypto not found')
libcrypto = CDLL(libcrypto)

View File

@@ -175,7 +175,7 @@ class ConfigWidget(QWidget):
def load_resource(self, name):
with ZipFile(self.plugin_path, 'r') as zf:
if name in zf.namelist():
return zf.read(name)
return zf.read(name).decode('utf-8')
return ""

View File

@@ -559,7 +559,7 @@ class DocParser(object):
if (link > 0):
linktype = self.link_type[link-1]
title = self.link_title[link-1]
title = title.rstrip(b'. ')
title = title.rstrip(b'. ').decode('utf-8')
alt_title = parares[lstart:]
alt_title = alt_title.strip()
# now strip off the actual printed page number
@@ -770,10 +770,10 @@ class DocParser(object):
first_para_continued = False
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
if not pclass:
if orig_regtype.endswith(b'.right') : pclass = 'cl-right'
elif orig_regtype.endswith(b'.center') : pclass = 'cl-center'
elif orig_regtype.endswith(b'.left') : pclass = 'cl-left'
elif orig_regtype.endswith(b'.justify') : pclass = 'cl-justify'
if orig_regtype.endswith(b'.right') : pclass = b'cl-right'
elif orig_regtype.endswith(b'.center') : pclass = b'cl-center'
elif orig_regtype.endswith(b'.left') : pclass = b'cl-left'
elif orig_regtype.endswith(b'.justify') : pclass = b'cl-justify'
if pclass and (ptype == 'full') and (len(pclass) >= 6):
tag = 'p'
if pclass[3:6] == b'h1-' : tag = 'h4'

View File

@@ -353,12 +353,16 @@ class Decryptor(object):
def decompress(self, bytes):
dc = zlib.decompressobj(-15)
bytes = dc.decompress(bytes)
ex = dc.decompress(b'Z') + dc.flush()
if ex:
bytes = bytes + ex
return bytes
try:
decompressed_bytes = dc.decompress(bytes)
ex = dc.decompress(b'Z') + dc.flush()
if ex:
decompressed_bytes = decompressed_bytes + ex
except:
# possibly not compressed by zip - just return bytes
return bytes
return decompressed_bytes
def decrypt(self, path, data):
if path.encode('utf-8') in self._encrypted:
data = self._aes.decrypt(data)[16:]
@@ -411,12 +415,12 @@ def decryptBook(userkey, inpath, outpath):
return 1
bookkey = rsa.decrypt(codecs.decode(bookkey.encode('ascii'), 'base64'))
# Padded as per RSAES-PKCS1-v1_5
if len(bookkey) != 16:
if bookkey[-17] != '\x00' and bookkey[-17] != 0:
if len(bookkey) > 16:
if bookkey[-17] == '\x00' or bookkey[-17] == 0:
bookkey = bookkey[-16:]
else:
print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)))
return 2
else:
bookkey = bookkey[-16:]
encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(bookkey, encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)

367
DeDRM_plugin/ineptpdf.py Normal file → Executable file
View File

@@ -54,12 +54,14 @@ Decrypts Adobe ADEPT-encrypted PDF files.
__license__ = 'GPL v3'
__version__ = "9.0.0"
import codecs
import sys
import os
import re
import zlib
import struct
import hashlib
from io import BytesIO
from decimal import Decimal
import itertools
import xml.etree.ElementTree as etree
@@ -258,7 +260,8 @@ def _load_crypto_pycrypto():
from Crypto.PublicKey import RSA as _RSA
from Crypto.Cipher import ARC4 as _ARC4
from Crypto.Cipher import AES as _AES
from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
# ASN.1 parsing code from tlslite
class ASN1Error(Exception):
pass
@@ -372,7 +375,7 @@ def _load_crypto_pycrypto():
class RSA(object):
def __init__(self, der):
key = ASN1Parser([ord(x) for x in der])
key = ASN1Parser([x for x in der])
key = [key.getChild(x).value for x in range(1, 4)]
key = [self.bytesToNumber(v) for v in key]
self._rsa = _RSA.construct(key)
@@ -384,7 +387,7 @@ def _load_crypto_pycrypto():
return total
def decrypt(self, data):
return self._rsa.decrypt(data)
return _PKCS1_v1_5.new(self._rsa).decrypt(data, 172)
return (ARC4, RSA, AES)
@@ -403,7 +406,6 @@ def _load_crypto():
ARC4, RSA, AES = _load_crypto()
from io import BytesIO
# Do we generate cross reference streams on output?
@@ -440,7 +442,7 @@ def nunpack(s, default=0):
elif l == 2:
return struct.unpack('>H', s)[0]
elif l == 3:
return struct.unpack('>L', '\x00'+s)[0]
return struct.unpack('>L', b'\x00'+s)[0]
elif l == 4:
return struct.unpack('>L', s)[0]
else:
@@ -472,16 +474,16 @@ class PSLiteral(PSObject):
Use PSLiteralTable.intern() instead.
'''
def __init__(self, name):
self.name = name
self.name = name.decode('utf-8')
return
def __repr__(self):
name = []
for char in self.name:
if not char.isalnum():
char = b'#%02x' % ord(char)
char = '#%02x' % ord(char)
name.append(char)
return b'/%s' % ''.join(name)
return '/%s' % ''.join(name)
# PSKeyword
class PSKeyword(PSObject):
@@ -491,7 +493,7 @@ class PSKeyword(PSObject):
Use PSKeywordTable.intern() instead.
'''
def __init__(self, name):
self.name = name
self.name = name.decode('utf-8')
return
def __repr__(self):
@@ -521,12 +523,12 @@ PSLiteralTable = PSSymbolTable(PSLiteral)
PSKeywordTable = PSSymbolTable(PSKeyword)
LIT = PSLiteralTable.intern
KWD = PSKeywordTable.intern
KEYWORD_BRACE_BEGIN = KWD('{')
KEYWORD_BRACE_END = KWD('}')
KEYWORD_ARRAY_BEGIN = KWD('[')
KEYWORD_ARRAY_END = KWD(']')
KEYWORD_DICT_BEGIN = KWD('<<')
KEYWORD_DICT_END = KWD('>>')
KEYWORD_BRACE_BEGIN = KWD(b'{')
KEYWORD_BRACE_END = KWD(b'}')
KEYWORD_ARRAY_BEGIN = KWD(b'[')
KEYWORD_ARRAY_END = KWD(b']')
KEYWORD_DICT_BEGIN = KWD(b'<<')
KEYWORD_DICT_END = KWD(b'>>')
def literal_name(x):
@@ -548,18 +550,18 @@ def keyword_name(x):
## PSBaseParser
##
EOL = re.compile(r'[\r\n]')
SPC = re.compile(r'\s')
NONSPC = re.compile(r'\S')
HEX = re.compile(r'[0-9a-fA-F]')
END_LITERAL = re.compile(r'[#/%\[\]()<>{}\s]')
END_HEX_STRING = re.compile(r'[^\s0-9a-fA-F]')
HEX_PAIR = re.compile(r'[0-9a-fA-F]{2}|.')
END_NUMBER = re.compile(r'[^0-9]')
END_KEYWORD = re.compile(r'[#/%\[\]()<>{}\s]')
END_STRING = re.compile(r'[()\134]')
OCT_STRING = re.compile(r'[0-7]')
ESC_STRING = { 'b':8, 't':9, 'n':10, 'f':12, 'r':13, '(':40, ')':41, '\\':92 }
EOL = re.compile(rb'[\r\n]')
SPC = re.compile(rb'\s')
NONSPC = re.compile(rb'\S')
HEX = re.compile(rb'[0-9a-fA-F]')
END_LITERAL = re.compile(rb'[#/%\[\]()<>{}\s]')
END_HEX_STRING = re.compile(rb'[^\s0-9a-fA-F]')
HEX_PAIR = re.compile(rb'[0-9a-fA-F]{2}|.')
END_NUMBER = re.compile(rb'[^0-9]')
END_KEYWORD = re.compile(rb'[#/%\[\]()<>{}\s]')
END_STRING = re.compile(rb'[()\\]')
OCT_STRING = re.compile(rb'[0-7]')
ESC_STRING = { b'b':8, b't':9, b'n':10, b'f':12, b'r':13, b'(':40, b')':41, b'\\':92 }
class PSBaseParser(object):
@@ -591,7 +593,6 @@ class PSBaseParser(object):
if not pos:
pos = self.bufpos+self.charpos
self.fp.seek(pos)
##print >>sys.stderr, 'poll(%d): %r' % (pos, self.fp.read(n))
self.fp.seek(pos0)
return
@@ -602,7 +603,7 @@ class PSBaseParser(object):
self.fp.seek(pos)
# reset the status for nextline()
self.bufpos = pos
self.buf = ''
self.buf = b''
self.charpos = 0
# reset the status for nexttoken()
self.parse1 = self.parse_main
@@ -624,32 +625,32 @@ class PSBaseParser(object):
if not m:
return (self.parse_main, len(s))
j = m.start(0)
c = s[j]
c = bytes([s[j]])
self.tokenstart = self.bufpos+j
if c == '%':
self.token = '%'
if c == b'%':
self.token = c
return (self.parse_comment, j+1)
if c == '/':
self.token = ''
if c == b'/':
self.token = b''
return (self.parse_literal, j+1)
if c in '-+' or c.isdigit():
if c in b'-+' or c.isdigit():
self.token = c
return (self.parse_number, j+1)
if c == '.':
if c == b'.':
self.token = c
return (self.parse_decimal, j+1)
if c.isalpha():
self.token = c
return (self.parse_keyword, j+1)
if c == '(':
self.token = ''
if c == b'(':
self.token = b''
self.paren = 1
return (self.parse_string, j+1)
if c == '<':
self.token = ''
if c == b'<':
self.token = b''
return (self.parse_wopen, j+1)
if c == '>':
self.token = ''
if c == b'>':
self.token = b''
return (self.parse_wclose, j+1)
self.add_token(KWD(c))
return (self.parse_main, j+1)
@@ -676,20 +677,20 @@ class PSBaseParser(object):
return (self.parse_literal, len(s))
j = m.start(0)
self.token += s[i:j]
c = s[j]
if c == '#':
self.hex = ''
c = bytes([s[j]])
if c == b'#':
self.hex = b''
return (self.parse_literal_hex, j+1)
self.add_token(LIT(self.token))
return (self.parse_main, j)
def parse_literal_hex(self, s, i):
c = s[i]
c = bytes([s[i]])
if HEX.match(c) and len(self.hex) < 2:
self.hex += c
return (self.parse_literal_hex, i+1)
if self.hex:
self.token += chr(int(self.hex, 16))
self.token += bytes([int(self.hex, 16)])
return (self.parse_literal, i)
def parse_number(self, s, i):
@@ -699,8 +700,8 @@ class PSBaseParser(object):
return (self.parse_number, len(s))
j = m.start(0)
self.token += s[i:j]
c = s[j]
if c == '.':
c = bytes([s[j]])
if c == b'.':
self.token += c
return (self.parse_decimal, j+1)
try:
@@ -716,7 +717,7 @@ class PSBaseParser(object):
return (self.parse_decimal, len(s))
j = m.start(0)
self.token += s[i:j]
self.add_token(Decimal(self.token))
self.add_token(Decimal(self.token.decode('utf-8')))
return (self.parse_main, j)
def parse_keyword(self, s, i):
@@ -742,58 +743,59 @@ class PSBaseParser(object):
return (self.parse_string, len(s))
j = m.start(0)
self.token += s[i:j]
c = s[j]
if c == '\\':
c = bytes([s[j]])
if c == b'\\':
self.oct = ''
return (self.parse_string_1, j+1)
if c == '(':
if c == b'(':
self.paren += 1
self.token += c
return (self.parse_string, j+1)
if c == ')':
if c == b')':
self.paren -= 1
if self.paren:
self.token += c
return (self.parse_string, j+1)
self.add_token(self.token)
return (self.parse_main, j+1)
def parse_string_1(self, s, i):
c = s[i]
c = bytes([s[i]])
if OCT_STRING.match(c) and len(self.oct) < 3:
self.oct += c
return (self.parse_string_1, i+1)
if self.oct:
self.token += chr(int(self.oct, 8))
self.token += bytes([int(self.oct, 8)])
return (self.parse_string, i)
if c in ESC_STRING:
self.token += chr(ESC_STRING[c])
self.token += bytes([ESC_STRING[c]])
return (self.parse_string, i+1)
def parse_wopen(self, s, i):
c = s[i]
c = bytes([s[i]])
if c.isspace() or HEX.match(c):
return (self.parse_hexstring, i)
if c == '<':
if c == b'<':
self.add_token(KEYWORD_DICT_BEGIN)
i += 1
return (self.parse_main, i)
def parse_wclose(self, s, i):
c = s[i]
if c == '>':
c = bytes([s[i]])
if c == b'>':
self.add_token(KEYWORD_DICT_END)
i += 1
return (self.parse_main, i)
def parse_hexstring(self, s, i):
m = END_HEX_STRING.search(s, i)
if not m:
m1 = END_HEX_STRING.search(s, i)
if not m1:
self.token += s[i:]
return (self.parse_hexstring, len(s))
j = m.start(0)
j = m1.start(0)
self.token += s[i:j]
token = HEX_PAIR.sub(lambda m: chr(int(m.group(0), 16)),
SPC.sub('', self.token))
token = HEX_PAIR.sub(lambda m2: bytes([int(m2.group(0), 16)]),
SPC.sub(b'', self.token))
self.add_token(token)
return (self.parse_main, j)
@@ -808,15 +810,15 @@ class PSBaseParser(object):
'''
Fetches a next line that ends either with \\r or \\n.
'''
linebuf = ''
linebuf = b''
linepos = self.bufpos + self.charpos
eol = False
while 1:
self.fillbuf()
if eol:
c = self.buf[self.charpos]
c = bytes([self.buf[self.charpos]])
# handle '\r\n'
if c == '\n':
if c == b'\n':
linebuf += c
self.charpos += 1
break
@@ -824,7 +826,7 @@ class PSBaseParser(object):
if m:
linebuf += self.buf[self.charpos:m.end(0)]
self.charpos = m.end(0)
if linebuf[-1] == '\r':
if bytes([linebuf[-1]]) == b'\r':
eol = True
else:
break
@@ -840,7 +842,7 @@ class PSBaseParser(object):
'''
self.fp.seek(0, 2)
pos = self.fp.tell()
buf = ''
buf = b''
while 0 < pos:
prevpos = pos
pos = max(0, pos-self.BUFSIZ)
@@ -848,13 +850,13 @@ class PSBaseParser(object):
s = self.fp.read(prevpos-pos)
if not s: break
while 1:
n = max(s.rfind('\r'), s.rfind('\n'))
n = max(s.rfind(b'\r'), s.rfind(b'\n'))
if n == -1:
buf = s + buf
break
yield s[n:]+buf
s = s[:n]
buf = ''
buf = b''
return
@@ -910,17 +912,17 @@ class PSStackParser(PSBaseParser):
def nextobject(self, direct=False):
'''
Yields a list of objects: keywords, literals, strings,
Yields a list of objects: keywords, literals, strings (byte arrays),
numbers, arrays and dictionaries. Arrays and dictionaries
are represented as Python sequence and dictionaries.
'''
while not self.results:
(pos, token) = self.nexttoken()
##print (pos,token), (self.curtype, self.curstack)
if (isinstance(token, int) or
isinstance(token, Decimal) or
isinstance(token, bool) or
isinstance(token, str) or
isinstance(token, bytearray) or
isinstance(token, bytes) or
isinstance(token, PSLiteral)):
# normal token
self.push((pos, token))
@@ -963,10 +965,10 @@ class PSStackParser(PSBaseParser):
return obj
LITERAL_CRYPT = PSLiteralTable.intern('Crypt')
LITERALS_FLATE_DECODE = (PSLiteralTable.intern('FlateDecode'), PSLiteralTable.intern('Fl'))
LITERALS_LZW_DECODE = (PSLiteralTable.intern('LZWDecode'), PSLiteralTable.intern('LZW'))
LITERALS_ASCII85_DECODE = (PSLiteralTable.intern('ASCII85Decode'), PSLiteralTable.intern('A85'))
LITERAL_CRYPT = LIT(b'Crypt')
LITERALS_FLATE_DECODE = (LIT(b'FlateDecode'), LIT(b'Fl'))
LITERALS_LZW_DECODE = (LIT(b'LZWDecode'), LIT(b'LZW'))
LITERALS_ASCII85_DECODE = (LIT(b'ASCII85Decode'), LIT(b'A85'))
## PDF Objects
@@ -1020,7 +1022,7 @@ def resolve_all(x):
if isinstance(x, list):
x = [ resolve_all(v) for v in x ]
elif isinstance(x, dict):
for (k,v) in x.iteritems():
for (k,v) in iter(x.items()):
x[k] = resolve_all(v)
return x
@@ -1028,13 +1030,13 @@ def decipher_all(decipher, objid, genno, x):
'''
Recursively decipher X.
'''
if isinstance(x, str):
if isinstance(x, bytearray) or isinstance(x,bytes):
return decipher(objid, genno, x)
decf = lambda v: decipher_all(decipher, objid, genno, v)
if isinstance(x, list):
x = [decf(v) for v in x]
elif isinstance(x, dict):
x = dict((k, decf(v)) for (k, v) in x.iteritems())
x = dict((k, decf(v)) for (k, v) in iter(x.items()))
return x
@@ -1065,7 +1067,7 @@ def num_value(x):
def str_value(x):
x = resolve1(x)
if not isinstance(x, str):
if not (isinstance(x, bytearray) or isinstance(x, bytes)):
if STRICT:
raise PDFTypeError('String required: %r' % x)
return ''
@@ -1166,7 +1168,6 @@ class PDFStream(PDFObject):
if 'Filter' not in self.dic:
self.data = data
self.rawdata = None
##print self.dict
return
filters = self.dic['Filter']
if not isinstance(filters, list):
@@ -1176,7 +1177,7 @@ class PDFStream(PDFObject):
# will get errors if the document is encrypted.
data = zlib.decompress(data)
elif f in LITERALS_LZW_DECODE:
data = ''.join(LZWDecoder(BytesIO(data)).run())
data = b''.join(LZWDecoder(BytesIO(data)).run())
elif f in LITERALS_ASCII85_DECODE:
data = ascii85decode(data)
elif f == LITERAL_CRYPT:
@@ -1204,7 +1205,7 @@ class PDFStream(PDFObject):
pred = data[i]
ent1 = data[i+1:i+1+columns]
if pred == b'\x02':
ent1 = ''.join(bytes([(a+b) & 255]) \
ent1 = b''.join(bytes([(a+b) & 255]) \
for (a,b) in zip(ent0,ent1))
buf += ent1
ent0 = ent1
@@ -1239,11 +1240,11 @@ class PDFEncryptionError(PDFException): pass
class PDFPasswordIncorrect(PDFEncryptionError): pass
# some predefined literals and keywords.
LITERAL_OBJSTM = PSLiteralTable.intern('ObjStm')
LITERAL_XREF = PSLiteralTable.intern('XRef')
LITERAL_PAGE = PSLiteralTable.intern('Page')
LITERAL_PAGES = PSLiteralTable.intern('Pages')
LITERAL_CATALOG = PSLiteralTable.intern('Catalog')
LITERAL_OBJSTM = LIT(b'ObjStm')
LITERAL_XREF = LIT(b'XRef')
LITERAL_PAGE = LIT(b'Page')
LITERAL_PAGES = LIT(b'Pages')
LITERAL_CATALOG = LIT(b'Catalog')
## XRefs
@@ -1261,7 +1262,7 @@ class PDFXRef(object):
return '<PDFXRef: objs=%d>' % len(self.offsets)
def objids(self):
return self.offsets.iterkeys()
return iter(self.offsets.keys())
def load(self, parser):
self.offsets = {}
@@ -1272,10 +1273,10 @@ class PDFXRef(object):
raise PDFNoValidXRef('Unexpected EOF - file corrupted?')
if not line:
raise PDFNoValidXRef('Premature eof: %r' % parser)
if line.startswith('trailer'):
if line.startswith(b'trailer'):
parser.seek(pos)
break
f = line.strip().split(' ')
f = line.strip().split(b' ')
if len(f) != 2:
raise PDFNoValidXRef('Trailer not found: %r: line=%r' % (parser, line))
try:
@@ -1287,16 +1288,17 @@ class PDFXRef(object):
(_, line) = parser.nextline()
except PSEOF:
raise PDFNoValidXRef('Unexpected EOF - file corrupted?')
f = line.strip().split(' ')
f = line.strip().split(b' ')
if len(f) != 3:
raise PDFNoValidXRef('Invalid XRef format: %r, line=%r' % (parser, line))
(pos, genno, use) = f
if use != 'n': continue
self.offsets[objid] = (int(genno), int(pos))
if use != b'n':
continue
self.offsets[objid] = (int(genno.decode('utf-8')), int(pos.decode('utf-8')))
self.load_trailer(parser)
return
KEYWORD_TRAILER = PSKeywordTable.intern('trailer')
KEYWORD_TRAILER = KWD(b'trailer')
def load_trailer(self, parser):
try:
(_,kwd) = parser.nexttoken()
@@ -1401,7 +1403,8 @@ class PDFDocument(object):
# set_parser(parser)
# Associates the document with an (already initialized) parser object.
def set_parser(self, parser):
if self.parser: return
if self.parser:
return
self.parser = parser
# The document is set to be temporarily ready during collecting
# all the basic information about the document, e.g.
@@ -1423,13 +1426,13 @@ class PDFDocument(object):
dict_value(trailer['Encrypt']))
# fix for bad files
except:
self.encryption = ('ffffffffffffffffffffffffffffffffffff',
self.encryption = (b'ffffffffffffffffffffffffffffffffffff',
dict_value(trailer['Encrypt']))
if 'Root' in trailer:
self.set_root(dict_value(trailer['Root']))
break
else:
raise PDFSyntaxError('No /Root object! - Is this really a PDF?')
else:
raise PDFSyntaxError('No /Root object! - Is this really a PDF?')
# The document is set to be non-ready again, until all the
# proper initialization (asking the password key and
# verifying the access permission, so on) is finished.
@@ -1450,7 +1453,7 @@ class PDFDocument(object):
# Perform the initialization with a given password.
# This step is mandatory even if there's no password associated
# with the document.
def initialize(self, password=''):
def initialize(self, password=b''):
if not self.encryption:
self.is_printable = self.is_modifiable = self.is_extractable = True
self.ready = True
@@ -1477,14 +1480,14 @@ class PDFDocument(object):
def genkey_adobe_ps(self, param):
# nice little offline principal keys dictionary
# global static principal key for German Onleihe / Bibliothek Digital
principalkeys = { 'bibliothek-digital.de': 'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw='.decode('base64')}
principalkeys = { b'bibliothek-digital.de': codecs.decode(b'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw=','base64')}
self.is_printable = self.is_modifiable = self.is_extractable = True
length = int_value(param.get('Length', 0)) / 8
length = int_value(param.get('Length', 0)) // 8
edcdata = str_value(param.get('EDCData')).decode('base64')
pdrllic = str_value(param.get('PDRLLic')).decode('base64')
pdrlpol = str_value(param.get('PDRLPol')).decode('base64')
edclist = []
for pair in edcdata.split('\n'):
for pair in edcdata.split(b'\n'):
edclist.append(pair)
# principal key request
for key in principalkeys:
@@ -1493,9 +1496,9 @@ class PDFDocument(object):
else:
raise ADEPTError('Cannot find principal key for this pdf')
shakey = SHA256(principalkey)
ivector = 16 * chr(0)
ivector = bytes(16) # 16 zero bytes
plaintext = AES.new(shakey,AES.MODE_CBC,ivector).decrypt(edclist[9].decode('base64'))
if plaintext[-16:] != 16 * chr(16):
if plaintext[-16:] != bytearray(b'\0x10')*16:
raise ADEPTError('Offlinekey cannot be decrypted, aborting ...')
pdrlpol = AES.new(plaintext[16:32],AES.MODE_CBC,edclist[2].decode('base64')).decrypt(pdrlpol)
if pdrlpol[-1] < 1 or pdrlpol[-1] > 16:
@@ -1505,8 +1508,8 @@ class PDFDocument(object):
pdrlpol = pdrlpol[:cutter]
return plaintext[:16]
PASSWORD_PADDING = '(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \
'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz'
PASSWORD_PADDING = b'(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \
b'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz'
# experimental aes pw support
def initialize_standard(self, password, docid, param):
# copy from a global variable
@@ -1523,7 +1526,7 @@ class PDFDocument(object):
try:
EncMetadata = str_value(param['EncryptMetadata'])
except:
EncMetadata = 'True'
EncMetadata = b'True'
self.is_printable = bool(P & 4)
self.is_modifiable = bool(P & 8)
self.is_extractable = bool(P & 16)
@@ -1540,12 +1543,12 @@ class PDFDocument(object):
hash.update(docid[0]) # 5
# aes special handling if metadata isn't encrypted
if EncMetadata == ('False' or 'false'):
hash.update('ffffffff'.decode('hex'))
hash.update(codecs.decode(b'ffffffff','hex'))
if 5 <= R:
# 8
for _ in range(50):
hash = hashlib.md5(hash.digest()[:length/8])
key = hash.digest()[:length/8]
hash = hashlib.md5(hash.digest()[:length//8])
key = hash.digest()[:length//8]
if R == 2:
# Algorithm 3.4
u1 = ARC4.new(key).decrypt(password)
@@ -1555,7 +1558,7 @@ class PDFDocument(object):
hash.update(docid[0]) # 3
x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4
for i in range(1,19+1):
k = ''.join(bytes([c ^ i]) for c in key )
k = b''.join(bytes([c ^ i]) for c in key )
x = ARC4.new(k).decrypt(x)
u1 = x+x # 32bytes total
if R == 2:
@@ -1587,17 +1590,19 @@ class PDFDocument(object):
def initialize_ebx(self, password, docid, param):
self.is_printable = self.is_modifiable = self.is_extractable = True
rsa = RSA(password)
length = int_value(param.get('Length', 0)) / 8
rights = str_value(param.get('ADEPT_LICENSE')).decode('base64')
length = int_value(param.get('Length', 0)) // 8
rights = codecs.decode(param.get('ADEPT_LICENSE'), 'base64')
rights = zlib.decompress(rights, -15)
rights = etree.fromstring(rights)
expr = './/{http://ns.adobe.com/adept}encryptedKey'
bookkey = ''.join(rights.findtext(expr)).decode('base64')
bookkey = codecs.decode(''.join(rights.findtext(expr)).encode('utf-8'),'base64')
bookkey = rsa.decrypt(bookkey)
if bookkey[0] != '\x02':
raise ADEPTError('error decrypting book session key')
index = bookkey.index('\0') + 1
bookkey = bookkey[index:]
#if bookkey[0] != 2:
# raise ADEPTError('error decrypting book session key')
if len(bookkey) > 16:
if bookkey[-17] == '\x00' or bookkey[-17] == 0:
bookkey = bookkey[-16:]
length = 16
ebx_V = int_value(param.get('V', 4))
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
# added because of improper booktype / decryption book session key errors
@@ -1643,7 +1648,7 @@ class PDFDocument(object):
objid = struct.pack('<L', objid ^ 0x3569ac)
genno = struct.pack('<L', genno ^ 0xca96)
key = self.decrypt_key
key += objid[0] + genno[0] + objid[1] + genno[1] + objid[2] + 'sAlT'
key += objid[0] + genno[0] + objid[1] + genno[1] + objid[2] + b'sAlT'
hash = hashlib.md5(key)
key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
return key
@@ -1652,7 +1657,7 @@ class PDFDocument(object):
def genkey_v4(self, objid, genno):
objid = struct.pack('<L', objid)[:3]
genno = struct.pack('<L', genno)[:2]
key = self.decrypt_key + objid + genno + 'sAlT'
key = self.decrypt_key + objid + genno + b'sAlT'
hash = hashlib.md5(key)
key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
return key
@@ -1664,7 +1669,6 @@ class PDFDocument(object):
plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data)
# remove pkcs#5 aes padding
cutter = -1 * plaintext[-1]
#print cutter
plaintext = plaintext[:cutter]
return plaintext
@@ -1675,7 +1679,6 @@ class PDFDocument(object):
plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data)
# remove pkcs#5 aes padding
cutter = -1 * plaintext[-1]
#print cutter
plaintext = plaintext[:cutter]
return plaintext
@@ -1684,7 +1687,7 @@ class PDFDocument(object):
return ARC4.new(key).decrypt(data)
KEYWORD_OBJ = PSKeywordTable.intern('obj')
KEYWORD_OBJ = KWD(b'obj')
def getobj(self, objid):
if not self.ready:
@@ -1791,11 +1794,11 @@ class PDFParser(PSStackParser):
def __repr__(self):
return '<PDFParser>'
KEYWORD_R = PSKeywordTable.intern('R')
KEYWORD_ENDOBJ = PSKeywordTable.intern('endobj')
KEYWORD_STREAM = PSKeywordTable.intern('stream')
KEYWORD_XREF = PSKeywordTable.intern('xref')
KEYWORD_STARTXREF = PSKeywordTable.intern('startxref')
KEYWORD_R = KWD(b'R')
KEYWORD_ENDOBJ = KWD(b'endobj')
KEYWORD_STREAM = KWD(b'stream')
KEYWORD_XREF = KWD(b'xref')
KEYWORD_STARTXREF = KWD(b'startxref')
def do_keyword(self, pos, token):
if token in (self.KEYWORD_XREF, self.KEYWORD_STARTXREF):
self.add_results(*self.pop(1))
@@ -1843,8 +1846,8 @@ class PDFParser(PSStackParser):
if STRICT:
raise PDFSyntaxError('Unexpected EOF')
break
if 'endstream' in line:
i = line.index('endstream')
if b'endstream' in line:
i = line.index(b'endstream')
objlen += i
data += line[:i]
break
@@ -1864,7 +1867,7 @@ class PDFParser(PSStackParser):
prev = None
for line in self.revreadlines():
line = line.strip()
if line == 'startxref': break
if line == b'startxref': break
if line:
prev = line
else:
@@ -1916,7 +1919,7 @@ class PDFParser(PSStackParser):
except PDFNoValidXRef:
# fallback
self.seek(0)
pat = re.compile(r'^(\d+)\s+(\d+)\s+obj\b')
pat = re.compile(rb'^(\d+)\s+(\d+)\s+obj\b')
offsets = {}
xref = PDFXRef()
while 1:
@@ -1924,7 +1927,7 @@ class PDFParser(PSStackParser):
(pos, line) = self.nextline()
except PSEOF:
break
if line.startswith('trailer'):
if line.startswith(b'trailer'):
trailerpos = pos # remember last trailer
m = pat.match(line)
if not m: continue
@@ -1951,7 +1954,7 @@ class PDFObjStrmParser(PDFParser):
self.add_results(*self.popall())
return
KEYWORD_R = KWD('R')
KEYWORD_R = KWD(b'R')
def do_keyword(self, pos, token):
if token is self.KEYWORD_R:
# reference to indirect object
@@ -1994,7 +1997,7 @@ class PDFSerializer(object):
def dump(self, outf):
self.outf = outf
self.write(self.version)
self.write('\n%\xe2\xe3\xcf\xd3\n')
self.write(b'\n%\xe2\xe3\xcf\xd3\n')
doc = self.doc
objids = self.objids
xrefs = {}
@@ -2016,18 +2019,18 @@ class PDFSerializer(object):
startxref = self.tell()
if not gen_xref_stm:
self.write('xref\n')
self.write('0 %d\n' % (maxobj + 1,))
self.write(b'xref\n')
self.write(b'0 %d\n' % (maxobj + 1,))
for objid in range(0, maxobj + 1):
if objid in xrefs:
# force the genno to be 0
self.write("%010d 00000 n \n" % xrefs[objid][0])
self.write(b"%010d 00000 n \n" % xrefs[objid][0])
else:
self.write("%010d %05d f \n" % (0, 65535))
self.write(b"%010d %05d f \n" % (0, 65535))
self.write('trailer\n')
self.write(b'trailer\n')
self.serialize_object(trailer)
self.write('\nstartxref\n%d\n%%%%EOF' % startxref)
self.write(b'\nstartxref\n%d\n%%%%EOF' % startxref)
else: # Generate crossref stream.
@@ -2076,7 +2079,7 @@ class PDFSerializer(object):
data.append(struct.pack('>L', f2)[-fl2:])
data.append(struct.pack('>L', f3)[-fl3:])
index.extend((first, prev - first + 1))
data = zlib.compress(''.join(data))
data = zlib.compress(b''.join(data))
dic = {'Type': LITERAL_XREF, 'Size': prev + 1, 'Index': index,
'W': [1, fl2, fl3], 'Length': len(data),
'Filter': LITERALS_FLATE_DECODE[0],
@@ -2085,7 +2088,7 @@ class PDFSerializer(object):
dic['Info'] = trailer['Info']
xrefstm = PDFStream(dic, data)
self.serialize_indirect(maxobj, xrefstm)
self.write('startxref\n%d\n%%%%EOF' % startxref)
self.write(b'startxref\n%d\n%%%%EOF' % startxref)
def write(self, data):
self.outf.write(data)
self.last = data[-1:]
@@ -2094,13 +2097,10 @@ class PDFSerializer(object):
return self.outf.tell()
def escape_string(self, string):
string = string.replace('\\', '\\\\')
string = string.replace('\n', r'\n')
string = string.replace('(', r'\(')
string = string.replace(')', r'\)')
# get rid of ciando id
regularexp = re.compile(r'http://www.ciando.com/index.cfm/intRefererID/\d{5}')
if regularexp.match(string): return ('http://www.ciando.com')
string = string.replace(b'\\', b'\\\\')
string = string.replace(b'\n', rb'\n')
string = string.replace(b'(', rb'\(')
string = string.replace(b')', rb'\)')
return string
def serialize_object(self, obj):
@@ -2111,34 +2111,38 @@ class PDFSerializer(object):
obj['Subtype'] = obj['Type']
del obj['Type']
# end - hope this doesn't have bad effects
self.write('<<')
self.write(b'<<')
for key, val in obj.items():
self.write('/%s' % key)
self.write(str(LIT(key.encode('utf-8'))).encode('utf-8'))
self.serialize_object(val)
self.write('>>')
self.write(b'>>')
elif isinstance(obj, list):
self.write('[')
self.write(b'[')
for val in obj:
self.serialize_object(val)
self.write(']')
self.write(b']')
elif isinstance(obj, bytearray):
self.write(b'(%s)' % self.escape_string(obj))
elif isinstance(obj, bytes):
self.write(b'(%s)' % self.escape_string(obj))
elif isinstance(obj, str):
self.write('(%s)' % self.escape_string(obj))
self.write(b'(%s)' % self.escape_string(obj.encode('utf-8')))
elif isinstance(obj, bool):
if self.last.isalnum():
self.write(' ')
self.write(str(obj).lower())
self.write(b' ')
self.write(str(obj).lower().encode('utf-8'))
elif isinstance(obj, int):
if self.last.isalnum():
self.write(' ')
self.write(str(obj))
self.write(b' ')
self.write(str(obj).encode('utf-8'))
elif isinstance(obj, Decimal):
if self.last.isalnum():
self.write(' ')
self.write(str(obj))
self.write(b' ')
self.write(str(obj).encode('utf-8'))
elif isinstance(obj, PDFObjRef):
if self.last.isalnum():
self.write(' ')
self.write('%d %d R' % (obj.objid, 0))
self.write(b' ')
self.write(b'%d %d R' % (obj.objid, 0))
elif isinstance(obj, PDFStream):
### If we don't generate cross ref streams the object streams
### are no longer useful, as we have extracted all objects from
@@ -2148,41 +2152,36 @@ class PDFSerializer(object):
else:
data = obj.get_decdata()
self.serialize_object(obj.dic)
self.write('stream\n')
self.write(b'stream\n')
self.write(data)
self.write('\nendstream')
self.write(b'\nendstream')
else:
data = str(obj)
if data[0].isalnum() and self.last.isalnum():
self.write(' ')
data = str(obj).encode('utf-8')
if bytes([data[0]]).isalnum() and self.last.isalnum():
self.write(b' ')
self.write(data)
def serialize_indirect(self, objid, obj):
self.write('%d 0 obj' % (objid,))
self.write(b'%d 0 obj' % (objid,))
self.serialize_object(obj)
if self.last.isalnum():
self.write('\n')
self.write('endobj\n')
self.write(b'\n')
self.write(b'endobj\n')
def decryptBook(userkey, inpath, outpath):
if RSA is None:
raise ADEPTError("PyCrypto or OpenSSL must be installed.")
raise ADEPTError("PyCryptodome or OpenSSL must be installed.")
with open(inpath, 'rb') as inf:
#try:
serializer = PDFSerializer(inf, userkey)
#except:
# print "Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))
# return 2
# hope this will fix the 'bad file descriptor' problem
with open(outpath, 'wb') as outf:
# help construct to make sure the method runs to the end
try:
serializer.dump(outf)
except Exception as e:
print("error writing pdf: {0}".format(e.args[0]))
print("error writing pdf: {0}".format(e))
return 2
return 0

View File

@@ -512,6 +512,8 @@ class BinaryIonParser(object):
if table is not None:
self.symbols.import_(table, min(maxid, len(table.symnames)))
if len(table.symnames) < maxid:
self.symbols.importunknown(name + "-unknown", maxid - len(table.symnames))
else:
self.symbols.importunknown(name, maxid)
@@ -733,7 +735,10 @@ SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0',
'com.amazon.drm.EncryptedPage@2.0',
'com.amazon.drm.PlainText@2.0', 'compression_algorithm',
'com.amazon.drm.Compressed@1.0', 'page_index_table',
'com.amazon.drm.VoucherEnvelope@2.0', 'com.amazon.drm.VoucherEnvelope@3.0' ]
] + ['com.amazon.drm.VoucherEnvelope@%d.0' % n
for n in list(range(2, 29)) + [
9708, 1031, 2069, 9041, 3646,
6052, 9479, 9888, 4648, 5683]]
def addprottable(ion):
ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
@@ -757,9 +762,45 @@ def pkcs7unpad(msg, blocklen):
# every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret
VOUCHER_VERSION_INFOS = {
2: [b'Antidisestablishmentarianism', 5],
3: [b'Floccinaucinihilipilification', 8]
OBFUSCATION_TABLE = {
"V1": (0x00, None),
"V2": (0x05, b'Antidisestablishmentarianism'),
"V3": (0x08, b'Floccinaucinihilipilification'),
"V4": (0x07, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'),
"V5": (0x06, b'~\x18~\x16J\\\x18\x10\x05\x0b\x07\t\x0cZ\r|\x1c\x15\x1d\x11>,\x1b\x0e\x03"4\x1b\x01'),
"V6": (0x09, b'3h\x055\x03[^>\x19\x1c\x08\x1b\rtm4\x02Rp\x0c\x16B\n'),
"V7": (0x05, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
"V8": (0x09, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
"V9": (0x05, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
"V10": (0x07, b'z3\n\x039\x12\x13`\x06=v,\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
"V11": (0x05, b'L=\nhVm\x07go\n6\x14\x06\x16L\r\x02\x0b\x0c\x1b\x04#p\t'),
"V12": (0x06, b',n\x1d\rl\x13\x1c\x13\x16p\x14\x07U\x0c\x1f\x19w\x16\x16\x1d5T'),
"V13": (0x07, b'I\x05\t\x08\x03r)\x01$N\x0fr3n\x0b062D\x0f\x13'),
"V14": (0x05, b"\x03\x02\x1c9\x19\x15\x15q\x1057\x08\x16\x0cF\x1b.Fw\x01\x12\x03\x13\x02\x17S'hk6"),
"V15": (0x0A, b'&,4B\x1dcI\x0bU\x03I\x07\x04\x1c\t\x05c\x07%ws\x0cj\t\x1a\x08\x0f'),
"V16": (0x0A, b'\x06\x18`h,b><\x06PqR\x02Zc\x034\n\x16\x1e\x18\x06#e'),
"V17": (0x07, b'y\r\x12\x08fw.[\x02\t\n\x13\x11\x0c\x11b\x1e8L\x10(\x13<Jx6c\x0f'),
"V18": (0x07, b'I\x0b\x0e,\x19\x1aIa\x10s\x19g\\\x1b\x11!\x18yf\x0f\t\x1d7[bSp\x03'),
"V19": (0x05, b'\n6>)N\x02\x188\x016s\x13\x14\x1b\x16jeN\n\x146\x04\x18\x1c\x0c\x19\x1f,\x02]'),
"V20": (0x08, b'_\r\x01\x12]\\\x14*\x17i\x14\r\t!\x1e,~hZ\x12jK\x17\x1e*1'),
"V21": (0x07, b'e\x1d\x19|\ty\x1di|N\x13\x0e\x04\x1bj<h\x13\x15k\x12\x08=\x1f\x16~\x13l'),
"V22": (0x08, b'?\x17yi$k7Pc\tEo\x0c\x07\x07\t\x1f,*i\x12\x0cI0\x10I\x1a?2\x04'),
"V23": (0x08, b'\x16+db\x13\x04\x18\rc%\x14\x17\x0f\x13F\x0c[\t9\x1ay\x01\x1eH'),
"V24": (0x06, b'|6\\\x1a\r\x10\nP\x07\x0fu\x1f\t,\rr`uv\\~55\x11]N'),
"V25": (0x09, b'\x07\x14w\x1e,^y\x01:\x08\x07\x1fr\tU#j\x16\x12\x1eB\x04\x16=\x06fZ\x07\x02\x06'),
"V26": (0x06, b'\x03IL\x1e"K\x1f\x0f\x1fp0\x01`X\x02z0`\x03\x0eN\x07'),
"V27": (0x07, b'Xk\x10y\x02\x18\x10\x17\x1d,\x0e\x05e\x10\x15"e\x0fh(\x06s\x1c\x08I\x0c\x1b\x0e'),
"V28": (0x0A, b'6P\x1bs\x0f\x06V.\x1cM\x14\x02\n\x1b\x07{P0:\x18zaU\x05'),
"V9708": (0x05, b'\x1diIm\x08a\x17\x1e!am\x1d\x1aQ.\x16!\x06*\}x04\x11\t\x06\x04?'),
"V1031": (0x08, b'Antidisestablishmentarianism'),
"V2069": (0x07, b'Floccinaucinihilipilification'),
"V9041": (0x06, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'),
"V3646": (0x09, b'~\x18~\x16J\\\x18\x10\x05\x0b\x07\t\x0cZ\r|\x1c\x15\x1d\x11>,\x1b\x0e\x03"4\x1b\x01'),
"V6052": (0x05, b'3h\x055\x03[^>\x19\x1c\x08\x1b\rtm4\x02Rp\x0c\x16B\n'),
"V9479": (0x09, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
"V9888": (0x05, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
"V4648": (0x07, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
"V5683": (0x05, b'z3\n\x039\x12\x13`\x06=v,\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
}
@@ -768,9 +809,7 @@ def obfuscate(secret, version):
if version == 1: # v1 does not use obfuscation
return secret
params = VOUCHER_VERSION_INFOS[version]
word = params[0]
magic = params[1]
magic, word = OBFUSCATION_TABLE["V%d" % version]
# extend secret so that its length is divisible by the magic number
if len(secret) % magic != 0:
@@ -809,24 +848,30 @@ class DrmIonVoucher(object):
def __init__(self, voucherenv, dsn, secret):
self.dsn, self.secret = dsn, secret
if isinstance(dsn, str):
self.dsn = dsn.encode('ASCII')
if isinstance(secret, str):
self.secret = secret.encode('ASCII')
self.lockparams = []
self.envelope = BinaryIonParser(voucherenv)
addprottable(self.envelope)
def decryptvoucher(self):
shared = "PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm
shared = ("PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm).encode('ASCII')
self.lockparams.sort()
for param in self.lockparams:
if param == "ACCOUNT_SECRET":
shared += param + self.secret
shared += param.encode('ASCII') + self.secret
elif param == "CLIENT_ID":
shared += param + self.dsn
shared += param.encode('ASCII') + self.dsn
else:
_assert(False, "Unknown lock parameter: %s" % param)
sharedsecret = obfuscate(shared.encode('ASCII'), self.version)
sharedsecret = obfuscate(shared, self.version)
key = hmac.new(sharedsecret, b"PIDv3", digestmod=hashlib.sha256).digest()
aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
@@ -993,6 +1038,7 @@ class DrmIon(object):
elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]:
decompress = False
decrypt = True
ct = None
civ = None
self.ion.stepin()
@@ -1006,7 +1052,23 @@ class DrmIon(object):
civ = self.ion.lobvalue()
if ct is not None and civ is not None:
self.processpage(ct, civ, outpages, decompress)
self.processpage(ct, civ, outpages, decompress, decrypt)
self.ion.stepout()
elif self.ion.gettypename() in ["com.amazon.drm.PlainText@1.0", "com.amazon.drm.PlainText@2.0"]:
decompress = False
decrypt = False
plaintext = None
self.ion.stepin()
while self.ion.hasnext():
self.ion.next()
if self.ion.gettypename() == "com.amazon.drm.Compressed@1.0":
decompress = True
if self.ion.getfieldname() == "data":
plaintext = self.ion.lobvalue()
if plaintext is not None:
self.processpage(plaintext, None, outpages, decompress, decrypt)
self.ion.stepout()
self.ion.stepout()
@@ -1017,9 +1079,12 @@ class DrmIon(object):
def print_(self, lst):
self.ion.print_(lst)
def processpage(self, ct, civ, outpages, decompress):
aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16])
msg = pkcs7unpad(aes.decrypt(ct), 16)
def processpage(self, ct, civ, outpages, decompress, decrypt):
if decrypt:
aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16])
msg = pkcs7unpad(aes.decrypt(ct), 16)
else:
msg = ct
if not decompress:
outpages.write(msg)

View File

@@ -191,7 +191,7 @@ def unescape(text):
else:
# named entity
try:
text = chr(htmlentitydefs.name2codepoint[text[1:-1]])
text = chr(html.entities.name2codepoint[text[1:-1]])
except KeyError:
pass
return text # leave as is

View File

@@ -4,10 +4,13 @@
# Engine to remove drm from Kindle KFX ebooks
# 2.0 - Python 3 for calibre 5.0
# 2.1 - Some fixes for debugging
# 2.1.1 - Whitespace!
import os
import shutil
import traceback
import zipfile
from io import BytesIO
@@ -65,6 +68,9 @@ class KFXZipBook:
print("Decrypting KFX DRM voucher: {0}".format(info.filename))
for pid in [''] + totalpids:
# Belt and braces. PIDs should be unicode strings, but just in case...
if isinstance(pid, bytes):
pid = pid.decode('ascii')
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,0), (40,40)]:
if len(pid) == dsn_len + secret_len:
break # split pid into DSN and account secret
@@ -77,6 +83,7 @@ class KFXZipBook:
voucher.decryptvoucher()
break
except:
traceback.print_exc()
pass
else:
raise Exception("Failed to decrypt KFX DRM voucher with any key")

View File

@@ -174,14 +174,14 @@ def pidFromSerial(s, l):
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
def getKindlePids(rec209, token, serialnum):
if isinstance(serialnum,str):
serialnum = serialnum.encode('utf-8')
if rec209 is None:
return [serialnum]
pids=[]
if isinstance(serialnum,str):
serialnum = serialnum.encode('utf-8')
# Compute book PID
pidHash = SHA1(serialnum+rec209+token)
bookPID = encodePID(pidHash)
@@ -209,7 +209,7 @@ def getK4Pids(rec209, token, kindleDatabase):
kindleAccountToken = bytearray.fromhex((kindleDatabase[1])['kindle.account.tokens'])
except KeyError:
kindleAccountToken=""
kindleAccountToken = b''
pass
try:
@@ -296,14 +296,14 @@ def getPidList(md1, md2, serials=[], kDatabases=[]):
for kDatabase in kDatabases:
try:
pidlst.extend(getK4Pids(md1, md2, kDatabase))
pidlst.extend(map(bytes,getK4Pids(md1, md2, kDatabase)))
except Exception as e:
print("Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0]))
traceback.print_exc()
for serialnum in serials:
try:
pidlst.extend(getKindlePids(md1, md2, serialnum))
pidlst.extend(map(bytes,getKindlePids(md1, md2, serialnum)))
except Exception as e:
print("Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0]))
traceback.print_exc()

36
DeDRM_plugin/mobidedrm.py Normal file → Executable file
View File

@@ -191,19 +191,21 @@ def PC1(key, src, decryption=True):
dst+=bytes([curByte])
return dst
# accepts unicode returns unicode
def checksumPid(s):
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
crc = (~binascii.crc32(s.encode('utf-8'),-1))&0xFFFFFFFF
crc = crc ^ (crc >> 16)
res = s
l = len(letters)
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += letters[pos%l].encode('ascii')
res += letters[pos%l]
crc >>= 8
return res
# expects bytearray
def getSizeOfTrailingDataEntries(ptr, size, flags):
def getSizeOfTrailingDataEntry(ptr, size):
bitpos, result = 0, 0
@@ -282,7 +284,7 @@ class MobiBook:
self.mobi_codepage = 1252
self.mobi_version = -1
if self.magic == 'TEXtREAd':
if self.magic == b'TEXtREAd':
print("PalmDoc format book detected.")
return
@@ -301,7 +303,7 @@ class MobiBook:
# if exth region exists parse it for metadata array
try:
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
exth = ''
exth = b''
if exth_flag & 0x40:
exth = self.sect[16 + self.mobi_length:]
if (len(exth) >= 12) and (exth[:4] == b'EXTH'):
@@ -323,12 +325,13 @@ class MobiBook:
except Exception as e:
print("Cannot set meta_array: Error: {:s}".format(e.args[0]))
#returns unicode
def getBookTitle(self):
codec_map = {
1252 : 'windows-1252',
65001 : 'utf-8',
}
title = ''
title = b''
codec = 'windows-1252'
if self.magic == b'BOOKMOBI':
if 503 in self.meta_array:
@@ -339,7 +342,7 @@ class MobiBook:
title = self.sect[toff:tend]
if self.mobi_codepage in codec_map.keys():
codec = codec_map[self.mobi_codepage]
if title == '':
if title == b'':
title = self.header[:32]
title = title.split(b'\0')[0]
return title.decode(codec)
@@ -355,13 +358,15 @@ class MobiBook:
# if that key exists in the meta_array, append its contents to the token
for i in range(0,len(data),5):
val, = struct.unpack('>I',data[i+1:i+5])
sval = self.meta_array.get(val,'')
sval = self.meta_array.get(val,b'')
token += sval
return rec209, token
# new must be byte array
def patch(self, off, new):
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
# new must be byte array
def patchSection(self, section, new, in_off = 0):
if (section + 1 == self.num_sections):
endoff = len(self.data_file)
@@ -371,12 +376,12 @@ class MobiBook:
assert off + in_off + len(new) <= endoff
self.patch(off + in_off, new)
# pids in pidlist must be unicode, returned key is byte array, pid is unicode
def parseDRM(self, data, count, pidlist):
found_key = None
keyvec1 = b'\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96'
for pid in pidlist:
bigpid = pid.ljust(16,b'\0')
bigpid = bigpid
bigpid = pid.encode('utf-8').ljust(16,b'\0')
temp_key = PC1(keyvec1, bigpid, False)
temp_key_sum = sum(temp_key) & 0xff
found_key = None
@@ -424,6 +429,7 @@ class MobiBook:
return ".azw3"
return ".mobi"
# pids in pidlist may be unicode or bytearrays or bytes
def processBook(self, pidlist):
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
print("Crypto Type is: {0:d}".format(crypto_type))
@@ -445,6 +451,8 @@ class MobiBook:
goodpids = []
# print("DEBUG ==== pidlist = ", pidlist)
for pid in pidlist:
if isinstance(pid,(bytearray,bytes)):
pid = pid.decode('utf-8')
if len(pid)==10:
if checksumPid(pid[0:-2]) != pid:
print("Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2])))
@@ -457,8 +465,8 @@ class MobiBook:
# print("======= DEBUG good pids = ", goodpids)
if self.crypto_type == 1:
t1_keyvec = 'QDCVEPMU675RUBSZ'
if self.magic == 'TEXtREAd':
t1_keyvec = b'QDCVEPMU675RUBSZ'
if self.magic == b'TEXtREAd':
bookkey_data = self.sect[0x0E:0x0E+16]
elif self.mobi_version < 0:
bookkey_data = self.sect[0x90:0x90+16]
@@ -473,7 +481,7 @@ class MobiBook:
raise DrmException("Encryption not initialised. Must be opened with Mobipocket Reader first.")
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
if not found_key:
raise DrmException("No key found in {0:d} keys tried.".format(len(goodpids)))
raise DrmException("No key found in {0:d} PIDs tried.".format(len(goodpids)))
# kill the drm keys
self.patchSection(0, b'\0' * drm_size, drm_ptr)
# kill the drm pointers
@@ -509,6 +517,7 @@ class MobiBook:
print("done")
return
# pids in pidlist must be unicode
def getUnencryptedBook(infile,pidlist):
if not os.path.isfile(infile):
raise DrmException("Input File Not Found.")
@@ -530,8 +539,7 @@ def cli_main():
infile = argv[1]
outfile = argv[2]
if len(argv) == 4:
# convert from unicode to bytearray before splitting.
pidlist = argv[3].encode('utf-8').split(b',')
pidlist = argv[3].split(',')
else:
pidlist = []
try:

View File

@@ -66,21 +66,21 @@ class fixZip:
def uncompress(self, cmpdata):
dc = zlib.decompressobj(-15)
data = ''
data = b''
while len(cmpdata) > 0:
if len(cmpdata) > _MAX_SIZE :
newdata = cmpdata[0:_MAX_SIZE]
cmpdata = cmpdata[_MAX_SIZE:]
else:
newdata = cmpdata
cmpdata = ''
cmpdata = b''
newdata = dc.decompress(newdata)
unprocessed = dc.unconsumed_tail
if len(unprocessed) == 0:
newdata += dc.flush()
data += newdata
cmpdata += unprocessed
unprocessed = ''
unprocessed = b''
return data
def getfiledata(self, zi):
@@ -123,7 +123,7 @@ class fixZip:
mimeinfo.internal_attr = 1 # text file
try:
# if the mimetype is present, get its info, including time-stamp
oldmimeinfo = self.inzip.getinfo('mimetype')
oldmimeinfo = self.inzip.getinfo(b'mimetype')
# copy across useful fields
mimeinfo.date_time = oldmimeinfo.date_time
mimeinfo.comment = oldmimeinfo.comment
@@ -137,7 +137,7 @@ class fixZip:
# write the rest of the files
for zinfo in self.inzip.infolist():
if zinfo.filename != "mimetype" or self.ztype != 'epub':
if zinfo.filename != b"mimetype" or self.ztype != 'epub':
data = None
try:
data = self.inzip.read(zinfo.filename)
@@ -153,6 +153,7 @@ class fixZip:
nzinfo.internal_attr=zinfo.internal_attr
nzinfo.external_attr=zinfo.external_attr
nzinfo.create_system=zinfo.create_system
nzinfo.flag_bits = zinfo.flag_bits & 0x800 # preserve UTF-8 flag
self.outzip.writestr(nzinfo,data)
self.bzf.close()

View File

@@ -27,12 +27,12 @@ Verify the one of the following cryptographic hash values, using software of you
#### Kindle for PC `KindleForPC-installer-1.17.44170.exe`:
* MD-5: 53F793B562F4823721AA47D7DE099869
* SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255
* SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B695176 2C120144163200
* SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B6951762C120144163200
#### Kindle for Mac `KindleForMac-44182.dmg`:
* MD-5: E7E36D5369E1F3CF1D28E5D9115DF15F
* SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C
* SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E 8A9E1D19EAE2AC.
* SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E8A9E1D19EAE2AC
You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the newer installation. You'll also need to delete the KFX folders from your My Kindle Content folder. You may also need to take further action to prevent an auto update. The simplest wayis to find the 'updates' folder and replace it with a file. See [this thread] (http://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead for a Script to do this on a PC. On a Mac you can find the folder at ~/Library/Application Support/Kindle/ just delete the folder 'updates' and save a blank text file called 'updates' in its place.
@@ -51,7 +51,9 @@ Mac Note: If the chmod command fails with a permission error try again using `su
After restarting the Kindle program any books previously downloaded in KFX format will no longer open. You will need to remove them from your device and re-download them. All future downloads will use the older Kindle formats instead of KFX although they will continue to be placed in one individual subdirectory per book. Note that books soudl be downoad by right-click and 'Download', not by just opening the book. Recent (1.25+) versions of Kindle for Mac/PC may convert KF8 files to a new format that is not supported by these tools when the book is opened for reading.
#### Decrypting KFX
Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for Mac/PC version 1.26 or earlier (version later than 1.26 use a new encryption scheme for KFX files). In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread.
Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for Mac/PC. In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread.
It's quite possible that Amazon will update their KFX DeDRM to prevent DRM removal from KFX books again. So Remove DRM as soon as possible!
#### Thanks
Thanks to jhowell for his investigations into KFX format and the KFX Input plugin. Some of these instructions are from [his thread on the subject](https://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead.

View File

@@ -3,7 +3,7 @@ from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__version__ = '6.7.0'
__version__ = '7.2.1'
__docformat__ = 'restructuredtext en'
#####################################################################
@@ -20,7 +20,7 @@ except NameError:
PLUGIN_NAME = 'Obok DeDRM'
PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.')
PLUGIN_VERSION_TUPLE = (6, 7, 0)
PLUGIN_VERSION_TUPLE = (7, 2, 1)
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm'
PLUGIN_AUTHORS = 'Anon'

View File

@@ -197,7 +197,7 @@ class InterfacePluginAction(InterfaceAction):
# We will write the help file out every time, in case the user upgrades the plugin zip
# and there is a newer help file contained within it.
file_path = os.path.join(config_dir, 'plugins', HELPFILE_NAME)
file_data = self.load_resources(HELPFILE_NAME)[HELPFILE_NAME]
file_data = self.load_resources(HELPFILE_NAME)[HELPFILE_NAME].decode('utf-8')
with open(file_path,'w') as f:
f.write(file_data)
return file_path

View File

@@ -1,6 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Version 4.1.0 February 2021
# Add detection for Kobo directory location on Linux
# Version 4.0.0 September 2020
# Python 3.0
#
@@ -365,9 +368,33 @@ class KoboLibrary(object):
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 linux_path != None:
# Probably Linux, let's get the wine prefix and path to Kobo.
# self.kobodir = os.path.join(linux_path, "Local Settings", "Application Data", "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
@@ -443,15 +470,15 @@ class KoboLibrary(object):
"""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)
output = subprocess.Popen('ipconfig /all', shell=True, stdout=subprocess.PIPE, text=True).stdout
c = re.compile('\s?(' + '[0-9a-f]{2}[:\-]' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
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)
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])

Binary file not shown.

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-11-17 12:51+0100\n"
"PO-Revision-Date: 2020-02-02 09:18+0100\n"
"PO-Revision-Date: 2021-01-19 12:20+0100\n"
"Language: sv\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -16,14 +16,14 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Last-Translator: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.2.4\n"
"X-Generator: Poedit 2.4.2\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
msgid ""
"<p>No books found in Kobo Library\n"
"Are you sure it's installed\\configured\\synchronized?"
msgstr ""
"<p>Inga böcker finns i Kobo-bibliotek\n"
"<p>Inga böcker hittades i Kobo-bibliotek\n"
"Är du säker på att den är installerad\\konfigurerad\\synkroniserad?"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
@@ -36,7 +36,7 @@ msgstr "Problem med att hämta nycklar med nyare obok-metod."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
msgid "Found {0} possible keys to try."
msgstr "Hittade {0} möjliga nycklar att pröva med."
msgstr "Hittade {0} möjliga nycklar att försöka med."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
@@ -46,7 +46,7 @@ msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
msgid "{} - Decryption canceled by user."
msgstr "{} - Dekryptering avbryts av användaren."
msgstr "{} - Dekryptering avbröts av användaren."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
msgid "{} - \"Add books\" canceled by user."
@@ -87,14 +87,14 @@ msgstr "dubblett upptäcktes"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
msgid "{0} - Successfully added EPUB format to existing {1}"
msgstr "{0} - Lade till EPUB-format till befintliga {1}"
msgstr "{0} - EPUB-formatet har lagts till i befintliga {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
msgid ""
"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
msgstr ""
"{0} - Fel vid tillägg av EPUB-format till befintligt {1}. Det här borde inte "
"hända."
"{0} - Fel vid tilläggning av EPUB-formatet till befintliga {1}. Detta borde "
"verkligen inte hända."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
msgid "{} - \"Insert formats\" canceled by user."
@@ -103,44 +103,44 @@ msgstr "{} - \"Infoga format\" avbröts av användaren."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
msgid ""
"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
msgstr "<p><b>{0}</b> EPUB{2} lades till bibliotek.<br /><br /><b>{1}</b> "
msgstr "<p><b>{0}</b> EPUB{2} har lagts till i bibliotek.<br /><br /><b>{1}</b> "
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
msgid ""
"not added because books with the same title/author were detected.<br /><br /"
">Would you like to try and add the EPUB format{0}"
msgstr ""
"inte tillagd eftersom böcker med samma titel/författare upptäcktes.<br/><br /"
">Vill du försöka lägga till EPUB-formatet{0}"
"lades inte till eftersom böcker med samma titel/författare upptäcktes.<br/"
"><br />Vill du försöka lägga till EPUB-formatet{0}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
msgid ""
" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
"overwritten."
msgstr ""
" till dessa befintliga poster?<br /><br />OBS: inga befintliga EPUB:er "
"kommer att skrivas över."
" till dessa befintliga poster?<br /><br />OBS: inga befintliga EPUB:er kommer "
"att skrivas över."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
msgid ""
"{0} -- not added because of {1} in your library.\n"
"\n"
msgstr ""
"{0} -- inte tillagd på grund av {1} i ditt bibliotek.\n"
"{0} -- lades inte till på grund av {1} i ditt bibliotek.\n"
"\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
msgstr ""
"<p><b>{0}</b> -- inte tillagd på grund av {1} i ditt bibliotek.<br /><br />"
"<p><b>{0}</b> -- lades inte till på grund av {1} i ditt bibliotek.<br /><br />"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
msgid ""
"Would you like to try and add the EPUB format to an available calibre "
"duplicate?<br /><br />"
msgstr ""
"Vill du försöka lägga till EPUB-formatet till en tillgänglig calibre-"
"dubblett?<br /><br />"
"Vill du försöka lägga till EPUB-formatet till en tillgänglig calibre-dubblett?"
"<br /><br />"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
msgid "NOTE: no pre-existing EPUB will be overwritten."
@@ -148,23 +148,23 @@ msgstr "OBS: ingen befintlig EPUB kommer att skrivas över."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
msgid "Trying key: "
msgstr "Prövar nyckel: "
msgstr "Försöker med nyckel: "
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
msgid "Decryption failed, trying next key."
msgstr "Det gick inte att dekryptera, prövar nästa nyckel."
msgstr "Dekryptering misslyckades, försöker med nästa nyckel."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
msgid "Unknown Error decrypting, trying next key.."
msgstr "Okänt fel dekryptering, prövar nästa nyckel.."
msgstr "Okänt fel vid dekryptering, försöker med nästa nyckel.."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
msgid ""
"<p>All selected Kobo books added as new calibre books or inserted into "
"existing calibre ebooks.<br /><br />No issues."
msgstr ""
"<p>Alla valda Kobo-böcker läggs till som nya calibre-böcker eller infogas i "
"befintliga calibre-e-böcker.<br /><br />Inga problem."
"<p>Alla valda Kobo-böcker har lagts till som nya calibre-böcker eller infogats "
"i befintliga calibre-e-böcker.<br /><br />Inga problem."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
msgid "<p>{0} successfully added."
@@ -192,15 +192,14 @@ msgstr "<p><b>Nya böcker skapade:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418
msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
msgstr "<p><b>Dubbletter som inte tillsattes:</b> {}</p>\n"
msgstr "<p><b>Dubbletter som inte har lagts till:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426
msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
msgstr "<p><b>Bokimport avbröts av användaren:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
msgid ""
"<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
msgid "<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
msgstr ""
"<p><b>Nya EPUB-format infogade i befintliga calibre-böcker:</b> {0}</p>\n"
@@ -208,8 +207,7 @@ msgstr ""
msgid ""
"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
msgstr ""
"<p><b>EPUB-format som INTE infogats i befintliga calibre-böcker:</b> {}<br /"
">\n"
"<p><b>EPUB-format som INTE infogats i befintliga calibre-böcker:</b> {}<br />\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
msgid ""
@@ -221,7 +219,7 @@ msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
msgstr "<p><b>Format-import avbröts av användaren:</b> {}</p>\n"
msgstr "<p><b>Format-importen avbröts av användaren:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
msgid "Unknown Book Title"
@@ -233,11 +231,11 @@ msgstr "den kunde inte dekrypteras."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
msgid ""
"user CHOSE not to insert the new EPUB format, or all existing calibre "
"entries HAD an EPUB format already."
"user CHOSE not to insert the new EPUB format, or all existing calibre entries "
"HAD an EPUB format already."
msgstr ""
"användaren VALDE att inte infoga det nya EPUB-formatet, eller alla "
"befintliga calibre-poster hade redan ett EPUB-format."
"användaren VALDE att inte infoga det nya EPUB-formatet, eller alla befintliga "
"calibre-poster hade redan ett EPUB-format."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
msgid "of unknown reasons. Gosh I'm embarrassed!"
@@ -245,7 +243,7 @@ msgstr "av okända skäl. Jag skäms!"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
msgid "<p>{0} not added because {1}"
msgstr "<p>{0} inte tillagd eftersom {1}"
msgstr "<p>{0} lades inte till på grund av {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
msgid "Help"
@@ -254,31 +252,31 @@ msgstr "Hjälp"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
msgid "Restart required"
msgstr "Omstart krävs"
msgstr "Kräver omstart"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
msgid ""
"Title image not found - you must restart Calibre before using this plugin!"
msgstr ""
"Titelbild hittades inte - du måste starta calibre innan du använder denna "
"Titelbild hittades inte - du måste starta om calibre innan du använder denna "
"insticksmodul!"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
msgid "Undefined"
msgstr "Obestämd"
msgstr "Odefinierad"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
msgid "When should Obok try to insert EPUBs into existing calibre entries?"
msgstr "När ska Obok försöka infoga EPUB:er i befintliga calibre-böcker?"
msgstr "När ska Obok försöka infoga EPUB:er i befintliga calibre-poster?"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
msgid ""
"<p>Default behavior when duplicates are detected. None of the choices will "
"cause calibre ebooks to be overwritten"
msgstr ""
"<p>Standardbeteende när dubbletter upptäcks. Inget av alternativen kommer "
"att orsaka calibre-e-böcker att skrivas över"
"<p>Standardbeteende när dubbletter upptäcks. Inget av alternativen kommer att "
"orsaka att calibre-e-böcker skrivs över"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Ask"
@@ -323,7 +321,7 @@ msgstr "Välj alla böcker med DRM."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
msgid "All DRM free"
msgstr "Alla DRM fria"
msgstr "Alla utan DRM"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
msgid "Select all books without DRM."
@@ -355,12 +353,12 @@ msgstr "Tar bort DRM från Kobo-kepubs och lägger till dem i biblioteket."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
msgid "AES improper key used"
msgstr "AES felaktig nyckel används"
msgstr "Felaktig AES-nyckel används"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
msgid "Failed to initialize AES key"
msgstr "Det gick inte att initiera AES-nyckel"
msgstr "Misslyckades med att initiera AES-nyckel"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
msgid "AES decryption failed"
msgstr "AES dekryptering misslyckades"
msgstr "AES-dekryptering misslyckades"

View File

@@ -2290,10 +2290,13 @@ class PDFDocument(object):
import win32api
import win32security
import win32file
import winreg
except:
raise ADEPTError('PyWin Extension (Win32API module) needed.\n'+\
'Download from http://sourceforge.net/projects/pywin32/files/ ')
try:
import winreg
except ImportError:
import _winreg as winreg
try:
v0 = win32api.GetVolumeInformation('C:\\')
v1 = win32api.GetSystemInfo()[6]

View File

@@ -10,14 +10,12 @@ The individual scripts are now released as two plugins for calibre: DeDRM and Ob
The DeDRM plugin handles books that use Amazon DRM, Adobe Digital Editions DRM (version 1), Barnes & Noble DRM, and some historical formats.
The Obok plugin handles Kobo DRM.
Users with calibre 5.x or later should use release 7.0.0b2 or later of the tools.
Users with calibe 4.x or earlier should use release 6.8.0 of the tools.
Developers might be interested in forking the repository, as it contains unzipped versions of those tools that are zipped to make the changes over time easier to follow.
Users with calibre 5.x or later should use release 7.2.0 or later of the tools.
Users with calibe 4.x or earlier should use release 6.8.x of the tools.
For the latest Amazon KFX format, users of the calibre plugin should also install the KFX Input plugin from the standard calibre plugin menu. It's also available from the MobileRead thread here: https://www.mobileread.com/forums/showthread.php?t=291290
Note that DRM can only be removed from KFX format files downloaded with Kindle for PC/Mac 1.26 or earlier. Amazon changes the DRM for KFX files in Kindle for PC/Mac 1.27 and later.
Note that Amazon changes the DRM for KFX files frequently. What works for KFX today might not work tomorrow.
I welcome contributions from others to improve these tools, from expanding the range of books handled, improving key retrieval, to just general bug fixes, speed improvements and UI enhancements.

View File

@@ -26,7 +26,7 @@ The tools are provided in the form of plugins for calibre. Calibre is an open so
DeDRM plugin for calibre (Mac OS X, Windows)
-------------------------------------------------------
calibe 5.x and later are now written in Python 3, and plugins must also use Python 3. If you have calibre 5, you must use version 7.x or later of the plugins. For calibre 4.x and earlier, use version 6.8.0 of the plugins.
calibe 5.x and later are now written in Python 3, and plugins must also use Python 3. If you have calibre 5, you must use version 7.x or later of the plugins. For calibre 4.x and earlier, use version 6.8.x of the plugins.
The DeDRM plugin for calibre removes DRM from your Kindle and Adobe DRM ebooks when they are imported to calibre. Just install the DeDRM plugin (DeDRM_plugin.zip), following the instructions and configuration directions provided in the ReadMe file and the help links in the plugin's configuration dialogs.