From 80cbaa4841622ffb4605f53107cc0bad0801ef35 Mon Sep 17 00:00:00 2001 From: NoDRM Date: Sat, 6 Aug 2022 13:53:03 +0200 Subject: [PATCH] Fix ZIP attribute "external_attr" getting reset --- CHANGELOG.md | 3 ++- DeDRM_plugin/epubfontdecrypt.py | 9 +++++++++ DeDRM_plugin/epubwatermark.py | 26 ++++++++++++++++++++++++++ DeDRM_plugin/ineptepub.py | 12 ++++++++++++ DeDRM_plugin/zeroedzipinfo.py | 30 ++++++++++++++++++++++++++++++ DeDRM_plugin/zipfilerugged.py | 13 +++++++++++++ DeDRM_plugin/zipfix.py | 30 ++++++++++++++++++++++-------- 7 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 DeDRM_plugin/zeroedzipinfo.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f3c7f2..783ce75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,4 +71,5 @@ 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 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). \ No newline at end of file +- 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. diff --git a/DeDRM_plugin/epubfontdecrypt.py b/DeDRM_plugin/epubfontdecrypt.py index ea08175..4baa375 100644 --- a/DeDRM_plugin/epubfontdecrypt.py +++ b/DeDRM_plugin/epubfontdecrypt.py @@ -25,6 +25,7 @@ import traceback import zlib import zipfile from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED +from zeroedzipinfo import ZeroedZipInfo from contextlib import closing from lxml import etree import itertools @@ -298,13 +299,21 @@ def decryptFontsBook(inpath, outpath): zi.internal_attr = oldzi.internal_attr # external attributes are dependent on the create system, so copy both. zi.external_attr = oldzi.external_attr + zi.volume = oldzi.volume 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 the file name or the comment contains any non-ASCII char, set the UTF8-flag zi.flag_bits |= 0x800 except: 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": outf.writestr(zi, inf.read('mimetype')) elif path == "META-INF/encryption.xml": diff --git a/DeDRM_plugin/epubwatermark.py b/DeDRM_plugin/epubwatermark.py index 176c77f..6719935 100644 --- a/DeDRM_plugin/epubwatermark.py +++ b/DeDRM_plugin/epubwatermark.py @@ -16,6 +16,7 @@ Removes various watermarks from EPUB files import traceback from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED +from zeroedzipinfo import ZeroedZipInfo from contextlib import closing from lxml import etree import re @@ -133,13 +134,22 @@ def removeHTMLwatermarks(object, path_to_ebook): zi.extra = oldzi.extra zi.internal_attr = oldzi.internal_attr zi.external_attr = oldzi.external_attr + zi.volume = oldzi.volume 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 the file name or the comment contains any non-ASCII char, set the UTF8-flag zi.flag_bits |= 0x800 except: 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) except: traceback.print_exc() @@ -249,13 +259,21 @@ def removeOPFwatermarks(object, path_to_ebook): zi.extra = oldzi.extra zi.internal_attr = oldzi.internal_attr zi.external_attr = oldzi.external_attr + zi.volume = oldzi.volume 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 the file name or the comment contains any non-ASCII char, set the UTF8-flag zi.flag_bits |= 0x800 except: 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) except: traceback.print_exc() @@ -301,13 +319,21 @@ def removeCDPwatermark(object, path_to_ebook): zi.extra = oldzi.extra zi.internal_attr = oldzi.internal_attr zi.external_attr = oldzi.external_attr + zi.volume = oldzi.volume 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 the file name or the comment contains any non-ASCII char, set the UTF8-flag zi.flag_bits |= 0x800 except: 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) print("Watermark: Successfully removed cdp.info watermark") diff --git a/DeDRM_plugin/ineptepub.py b/DeDRM_plugin/ineptepub.py index 6b4b676..094cb8c 100644 --- a/DeDRM_plugin/ineptepub.py +++ b/DeDRM_plugin/ineptepub.py @@ -48,6 +48,7 @@ import base64 import zlib import zipfile from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED +from zeroedzipinfo import ZeroedZipInfo from contextlib import closing from lxml import etree from uuid import UUID @@ -356,12 +357,23 @@ def decryptBook(userkey, inpath, outpath): zi.internal_attr = oldzi.internal_attr # external attributes are dependent on the create system, so copy both. zi.external_attr = oldzi.external_attr + + zi.volume = oldzi.volume 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 the file name or the comment contains any non-ASCII char, set the UTF8-flag zi.flag_bits |= 0x800 except: 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": outf.writestr(zi, data) else: diff --git a/DeDRM_plugin/zeroedzipinfo.py b/DeDRM_plugin/zeroedzipinfo.py new file mode 100644 index 0000000..08c65d0 --- /dev/null +++ b/DeDRM_plugin/zeroedzipinfo.py @@ -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) diff --git a/DeDRM_plugin/zipfilerugged.py b/DeDRM_plugin/zipfilerugged.py index aef9ea3..1941cc0 100755 --- a/DeDRM_plugin/zipfilerugged.py +++ b/DeDRM_plugin/zipfilerugged.py @@ -394,6 +394,19 @@ class ZipInfo (object): 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 to handle decryption of files stored within a ZIP archive. diff --git a/DeDRM_plugin/zipfix.py b/DeDRM_plugin/zipfix.py index 3fbfbce..9cb4ff1 100644 --- a/DeDRM_plugin/zipfix.py +++ b/DeDRM_plugin/zipfix.py @@ -26,6 +26,7 @@ import sys, os import zlib import zipfilerugged +from zipfilerugged import ZipInfo, ZeroedZipInfo import getopt from struct import unpack @@ -36,12 +37,6 @@ _FILENAME_OFFSET = 30 _MAX_SIZE = 64 * 1024 _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: def __init__(self, zinput, zoutput): @@ -117,7 +112,8 @@ class fixZip: # if epub write mimetype file first, with no compression if self.ztype == 'epub': # 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 try: # if the mimetype is present, get its info, including time-stamp @@ -129,8 +125,16 @@ class fixZip: mimeinfo.internal_attr = oldmimeinfo.internal_attr mimeinfo.external_attr = oldmimeinfo.external_attr mimeinfo.create_system = oldmimeinfo.create_system + mimeinfo.create_version = oldmimeinfo.create_version + mimeinfo.volume = oldmimeinfo.volume except: 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')) # write the rest of the files @@ -145,13 +149,23 @@ class fixZip: zinfo.filename = local_name # 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.extra=zinfo.extra nzinfo.internal_attr=zinfo.internal_attr nzinfo.external_attr=zinfo.external_attr 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 + + # 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.bzf.close()