Compare commits

...

2 Commits

Author SHA1 Message Date
NoDRM
41df9ecda0 Fix PDF corruption in Calibre 4 (#104) 2022-08-06 15:29:45 +02:00
NoDRM
80cbaa4841 Fix ZIP attribute "external_attr" getting reset 2022-08-06 13:53:03 +02:00
8 changed files with 144 additions and 18 deletions

View File

@@ -71,4 +71,6 @@ List of changes since the fork of Apprentice Harper's repository:
- Fix a bug introduced with #48 that breaks DeDRM'ing on Calibre 4 (fixes #101). - Fix a bug introduced with #48 that breaks DeDRM'ing on Calibre 4 (fixes #101).
- Fix some more Calibre-6 bugs in the Obok plugin (should fix #114). - Fix some more Calibre-6 bugs in the Obok plugin (should fix #114).
- Fix a bug where invalid Adobe keys could cause the plugin to stop trying subsequent keys (partially fixes #109). - Fix a bug where invalid Adobe keys could cause the plugin to stop trying subsequent keys (partially fixes #109).
- Fix DRM removal sometimes resetting the ZIP's internal "external_attr" value on Calibre 5 and newer.
- Fix PDF decryption issues on Calibre 4 (hopefully fixes #104).

View File

@@ -25,6 +25,7 @@ import traceback
import zlib import zlib
import zipfile import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from zeroedzipinfo import ZeroedZipInfo
from contextlib import closing from contextlib import closing
from lxml import etree from lxml import etree
import itertools import itertools
@@ -298,13 +299,21 @@ def decryptFontsBook(inpath, outpath):
zi.internal_attr = oldzi.internal_attr zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both. # external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr zi.external_attr = oldzi.external_attr
zi.volume = oldzi.volume
zi.create_system = oldzi.create_system zi.create_system = oldzi.create_system
zi.create_version = oldzi.create_version
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment): if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag # If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800 zi.flag_bits |= 0x800
except: except:
pass pass
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if zi.external_attr == 0:
zi = ZeroedZipInfo(zi)
if path == "mimetype": if path == "mimetype":
outf.writestr(zi, inf.read('mimetype')) outf.writestr(zi, inf.read('mimetype'))
elif path == "META-INF/encryption.xml": elif path == "META-INF/encryption.xml":

View File

@@ -16,6 +16,7 @@ Removes various watermarks from EPUB files
import traceback import traceback
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from zeroedzipinfo import ZeroedZipInfo
from contextlib import closing from contextlib import closing
from lxml import etree from lxml import etree
import re import re
@@ -133,13 +134,22 @@ def removeHTMLwatermarks(object, path_to_ebook):
zi.extra = oldzi.extra zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr zi.internal_attr = oldzi.internal_attr
zi.external_attr = oldzi.external_attr zi.external_attr = oldzi.external_attr
zi.volume = oldzi.volume
zi.create_system = oldzi.create_system zi.create_system = oldzi.create_system
zi.create_version = oldzi.create_version
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment): if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag # If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800 zi.flag_bits |= 0x800
except: except:
pass pass
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if zi.external_attr == 0:
zi = ZeroedZipInfo(zi)
outf.writestr(zi, data) outf.writestr(zi, data)
except: except:
traceback.print_exc() traceback.print_exc()
@@ -249,13 +259,21 @@ def removeOPFwatermarks(object, path_to_ebook):
zi.extra = oldzi.extra zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr zi.internal_attr = oldzi.internal_attr
zi.external_attr = oldzi.external_attr zi.external_attr = oldzi.external_attr
zi.volume = oldzi.volume
zi.create_system = oldzi.create_system zi.create_system = oldzi.create_system
zi.create_version = oldzi.create_version
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment): if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag # If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800 zi.flag_bits |= 0x800
except: except:
pass pass
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if zi.external_attr == 0:
zi = ZeroedZipInfo(zi)
outf.writestr(zi, data) outf.writestr(zi, data)
except: except:
traceback.print_exc() traceback.print_exc()
@@ -301,13 +319,21 @@ def removeCDPwatermark(object, path_to_ebook):
zi.extra = oldzi.extra zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr zi.internal_attr = oldzi.internal_attr
zi.external_attr = oldzi.external_attr zi.external_attr = oldzi.external_attr
zi.volume = oldzi.volume
zi.create_system = oldzi.create_system zi.create_system = oldzi.create_system
zi.create_version = oldzi.create_version
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment): if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag # If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800 zi.flag_bits |= 0x800
except: except:
pass pass
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if zi.external_attr == 0:
zi = ZeroedZipInfo(zi)
outf.writestr(zi, data) outf.writestr(zi, data)
print("Watermark: Successfully removed cdp.info watermark") print("Watermark: Successfully removed cdp.info watermark")

View File

@@ -48,6 +48,7 @@ import base64
import zlib import zlib
import zipfile import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from zeroedzipinfo import ZeroedZipInfo
from contextlib import closing from contextlib import closing
from lxml import etree from lxml import etree
from uuid import UUID from uuid import UUID
@@ -356,12 +357,23 @@ def decryptBook(userkey, inpath, outpath):
zi.internal_attr = oldzi.internal_attr zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both. # external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr zi.external_attr = oldzi.external_attr
zi.volume = oldzi.volume
zi.create_system = oldzi.create_system zi.create_system = oldzi.create_system
zi.create_version = oldzi.create_version
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment): if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag # If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800 zi.flag_bits |= 0x800
except: except:
pass pass
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if zi.external_attr == 0:
zi = ZeroedZipInfo(zi)
if path == "META-INF/encryption.xml": if path == "META-INF/encryption.xml":
outf.writestr(zi, data) outf.writestr(zi, data)
else: else:

View File

@@ -51,13 +51,14 @@
# 9.1.1 - Only support PyCryptodome; clean up the code # 9.1.1 - Only support PyCryptodome; clean up the code
# 10.0.0 - Add support for "hardened" Adobe DRM (RMSDK >= 10) # 10.0.0 - Add support for "hardened" Adobe DRM (RMSDK >= 10)
# 10.0.2 - Fix some Python2 stuff # 10.0.2 - Fix some Python2 stuff
# 10.0.4 - Fix more Python2 stuff
""" """
Decrypts Adobe ADEPT-encrypted PDF files. Decrypts Adobe ADEPT-encrypted PDF files.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "10.0.2" __version__ = "10.0.4"
import codecs import codecs
import hashlib import hashlib
@@ -200,7 +201,10 @@ def nunpack(s, default=0):
elif l == 2: elif l == 2:
return struct.unpack('>H', s)[0] return struct.unpack('>H', s)[0]
elif l == 3: elif l == 3:
return struct.unpack('>L', bytes([0]) + s)[0] if sys.version_info[0] == 2:
return struct.unpack('>L', '\x00'+s)[0]
else:
return struct.unpack('>L', bytes([0]) + s)[0]
elif l == 4: elif l == 4:
return struct.unpack('>L', s)[0] return struct.unpack('>L', s)[0]
else: else:
@@ -459,7 +463,10 @@ class PSBaseParser(object):
self.hex += c self.hex += c
return (self.parse_literal_hex, i+1) return (self.parse_literal_hex, i+1)
if self.hex: if self.hex:
self.token += bytes([int(self.hex, 16)]) if sys.version_info[0] == 2:
self.token += chr(int(self.hex, 16))
else:
self.token += bytes([int(self.hex, 16)])
return (self.parse_literal, i) return (self.parse_literal, i)
def parse_number(self, s, i): def parse_number(self, s, i):
@@ -543,10 +550,18 @@ class PSBaseParser(object):
self.oct += c self.oct += c
return (self.parse_string_1, i+1) return (self.parse_string_1, i+1)
if self.oct: if self.oct:
self.token += bytes([int(self.oct, 8)]) if sys.version_info[0] == 2:
self.token += chr(int(self.oct, 8))
else:
self.token += bytes([int(self.oct, 8)])
return (self.parse_string, i) return (self.parse_string, i)
if c in ESC_STRING: if c in ESC_STRING:
self.token += bytes([ESC_STRING[c]])
if sys.version_info[0] == 2:
self.token += chr(ESC_STRING[c])
else:
self.token += bytes([ESC_STRING[c]])
return (self.parse_string, i+1) return (self.parse_string, i+1)
def parse_wopen(self, s, i): def parse_wopen(self, s, i):
@@ -572,14 +587,19 @@ class PSBaseParser(object):
return (self.parse_main, i) return (self.parse_main, i)
def parse_hexstring(self, s, i): def parse_hexstring(self, s, i):
m1 = END_HEX_STRING.search(s, i) m = END_HEX_STRING.search(s, i)
if not m1: if not m:
self.token += s[i:] self.token += s[i:]
return (self.parse_hexstring, len(s)) return (self.parse_hexstring, len(s))
j = m1.start(0) j = m.start(0)
self.token += s[i:j] self.token += s[i:j]
token = HEX_PAIR.sub(lambda m2: bytes([int(m2.group(0), 16)]), if sys.version_info[0] == 2:
token = HEX_PAIR.sub(lambda m: chr(int(m.group(0), 16)),
SPC.sub('', self.token))
else:
token = HEX_PAIR.sub(lambda m: bytes([int(m.group(0), 16)]),
SPC.sub(b'', self.token)) SPC.sub(b'', self.token))
self.add_token(token) self.add_token(token)
return (self.parse_main, j) return (self.parse_main, j)

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Python 3's "zipfile" has an annoying bug where the `external_attr` field
of a ZIP file cannot be set to 0. However, if the original DRMed ZIP has
that set to 0 then we want the DRM-free ZIP to have that as 0, too.
See https://github.com/python/cpython/issues/87713
We cannot just set the "external_attr" to 0 as the code to save the ZIP
resets that variable.
So, here's a class that inherits from ZipInfo and ensures that EVERY
read access to that variable will return a 0 ...
"""
import zipfile
class ZeroedZipInfo(zipfile.ZipInfo):
def __init__(self, zinfo):
for k in self.__slots__:
if hasattr(zinfo, k):
setattr(self, k, getattr(zinfo, k))
def __getattribute__(self, name):
if name == "external_attr":
return 0
return object.__getattribute__(self, name)

View File

@@ -394,6 +394,19 @@ class ZipInfo (object):
extra = extra[ln+4:] extra = extra[ln+4:]
class ZeroedZipInfo(ZipInfo):
def __init__(self, zinfo):
for k in self.__slots__:
if hasattr(zinfo, k):
setattr(self, k, getattr(zinfo, k))
def __getattribute__(self, name):
if name == "external_attr":
return 0
return object.__getattribute__(self, name)
class _ZipDecrypter: class _ZipDecrypter:
"""Class to handle decryption of files stored within a ZIP archive. """Class to handle decryption of files stored within a ZIP archive.

View File

@@ -26,6 +26,7 @@ import sys, os
import zlib import zlib
import zipfilerugged import zipfilerugged
from zipfilerugged import ZipInfo, ZeroedZipInfo
import getopt import getopt
from struct import unpack from struct import unpack
@@ -36,12 +37,6 @@ _FILENAME_OFFSET = 30
_MAX_SIZE = 64 * 1024 _MAX_SIZE = 64 * 1024
_MIMETYPE = 'application/epub+zip' _MIMETYPE = 'application/epub+zip'
class ZipInfo(zipfilerugged.ZipInfo):
def __init__(self, *args, **kwargs):
if 'compress_type' in kwargs:
compress_type = kwargs.pop('compress_type')
super(ZipInfo, self).__init__(*args, **kwargs)
self.compress_type = compress_type
class fixZip: class fixZip:
def __init__(self, zinput, zoutput): def __init__(self, zinput, zoutput):
@@ -117,7 +112,8 @@ class fixZip:
# if epub write mimetype file first, with no compression # if epub write mimetype file first, with no compression
if self.ztype == 'epub': if self.ztype == 'epub':
# first get a ZipInfo with current time and no compression # first get a ZipInfo with current time and no compression
mimeinfo = ZipInfo(b'mimetype',compress_type=zipfilerugged.ZIP_STORED) mimeinfo = ZipInfo(b'mimetype')
mimeinfo.compress_type = zipfilerugged.ZIP_STORED
mimeinfo.internal_attr = 1 # text file mimeinfo.internal_attr = 1 # text file
try: try:
# if the mimetype is present, get its info, including time-stamp # if the mimetype is present, get its info, including time-stamp
@@ -129,8 +125,16 @@ class fixZip:
mimeinfo.internal_attr = oldmimeinfo.internal_attr mimeinfo.internal_attr = oldmimeinfo.internal_attr
mimeinfo.external_attr = oldmimeinfo.external_attr mimeinfo.external_attr = oldmimeinfo.external_attr
mimeinfo.create_system = oldmimeinfo.create_system mimeinfo.create_system = oldmimeinfo.create_system
mimeinfo.create_version = oldmimeinfo.create_version
mimeinfo.volume = oldmimeinfo.volume
except: except:
pass pass
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if mimeinfo.external_attr == 0:
mimeinfo = ZeroedZipInfo(mimeinfo)
self.outzip.writestr(mimeinfo, _MIMETYPE.encode('ascii')) self.outzip.writestr(mimeinfo, _MIMETYPE.encode('ascii'))
# write the rest of the files # write the rest of the files
@@ -145,13 +149,23 @@ class fixZip:
zinfo.filename = local_name zinfo.filename = local_name
# create new ZipInfo with only the useful attributes from the old info # create new ZipInfo with only the useful attributes from the old info
nzinfo = ZipInfo(zinfo.filename, zinfo.date_time, compress_type=zinfo.compress_type) nzinfo = ZipInfo(zinfo.filename)
nzinfo.date_time = zinfo.date_time
nzinfo.compress_type = zinfo.compress_type
nzinfo.comment=zinfo.comment nzinfo.comment=zinfo.comment
nzinfo.extra=zinfo.extra nzinfo.extra=zinfo.extra
nzinfo.internal_attr=zinfo.internal_attr nzinfo.internal_attr=zinfo.internal_attr
nzinfo.external_attr=zinfo.external_attr nzinfo.external_attr=zinfo.external_attr
nzinfo.create_system=zinfo.create_system nzinfo.create_system=zinfo.create_system
nzinfo.create_version = zinfo.create_version
nzinfo.volume = zinfo.volume
nzinfo.flag_bits = zinfo.flag_bits & 0x800 # preserve UTF-8 flag nzinfo.flag_bits = zinfo.flag_bits & 0x800 # preserve UTF-8 flag
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if nzinfo.external_attr == 0:
nzinfo = ZeroedZipInfo(nzinfo)
self.outzip.writestr(nzinfo,data) self.outzip.writestr(nzinfo,data)
self.bzf.close() self.bzf.close()