Add support for "hardened" Adobe DRM

What took the most time was not reverse-engineering
the scheme, but actually finding books using it...

Closes #20, #25, #45
This commit is contained in:
a980e066a01
2022-02-22 23:47:51 +00:00
committed by noDRM
parent a1dd63ae5f
commit c5aebcca01
7 changed files with 66 additions and 39 deletions

View File

@@ -175,7 +175,7 @@ def getfiledata(file, zi):
return data
def encryption(infile):
# Supports Adobe (old & new), B&N, Kobo, Apple, Readium LCP.
# Supports Adobe (old & new), B&N, Kobo, Apple, Readium LCP.
encryption = "Error"
try:
with open(infile,'rb') as infileobject:
@@ -206,10 +206,8 @@ def encryption(infile):
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr))
if len(bookkey) == 172:
encryption = "Adobe (old)"
if len(bookkey) == 192:
encryption = "Adobe (new)"
if len(bookkey) >= 172:
encryption = "Adobe"
elif len(bookkey) == 64:
encryption = "B&N"
else:

View File

@@ -32,13 +32,14 @@
# 7.0 - Add Python 3 compatibility for calibre 5.0
# 7.1 - Add ignoble support, dropping the dedicated ignobleepub.py script
# 7.2 - Only support PyCryptodome; clean up the code
# 8.0 - Add support for "hardened" Adobe DRM (RMSDK >= 10)
"""
Decrypt Adobe Digital Editions encrypted ePub books.
"""
__license__ = 'GPL v3'
__version__ = "7.2"
__version__ = "8.0"
import sys
import os
@@ -49,6 +50,8 @@ import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from contextlib import closing
from lxml import etree
from uuid import UUID
import hashlib
try:
from Cryptodome.Cipher import AES, PKCS1_v1_5
@@ -247,6 +250,23 @@ def adeptGetUserUUID(inpath):
except:
return None
def removeHardening(rights, keytype, keydata):
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
textGetter = lambda name: ''.join(rights.findtext('.//%s' % (adept(name),)))
# Gather what we need, and generate the IV
resourceuuid = UUID(textGetter("resource"))
deviceuuid = UUID(textGetter("device"))
fullfillmentuuid = UUID(textGetter("fulfillment")[:36])
kekiv = UUID(int=resourceuuid.int ^ deviceuuid.int ^ fullfillmentuuid.int).bytes
# Derive kek from just "keytype"
rem = int(keytype, 10) % 16
H = hashlib.sha256(keytype.encode("ascii")).digest()
kek = H[2*rem : 16 + rem] + H[rem : 2*rem]
return unpad(AES.new(kek, AES.MODE_CBC, kekiv).decrypt(keydata), 16) # PKCS#7
def decryptBook(userkey, inpath, outpath):
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = inf.namelist()
@@ -260,15 +280,12 @@ def decryptBook(userkey, inpath, outpath):
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr))
if len(bookkey) == 192:
print("{0:s} seems to be an Adobe ADEPT ePub with Adobe's new DRM".format(os.path.basename(inpath)))
print("This DRM cannot be removed yet. ")
print("Try getting your distributor to give you a new ACSM file, then open that in an old version of ADE (2.0).")
print("If your book distributor is not enforcing the new DRM yet, this will give you a copy with the old DRM.")
raise ADEPTNewVersionError("Book uses new ADEPT encryption")
if len(bookkey) == 172:
bookkeyelem = rights.find(expr)
bookkey = bookkeyelem.text
keytype = bookkeyelem.attrib.get('keyType', '0')
if len(bookkey) >= 172 and int(keytype, 10) > 2:
print("{0:s} is a secure Adobe Adept ePub with hardening.".format(os.path.basename(inpath)))
elif len(bookkey) == 172:
print("{0:s} is a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
elif len(bookkey) == 64:
print("{0:s} is a secure Adobe PassHash (B&N) ePub.".format(os.path.basename(inpath)))
@@ -277,9 +294,11 @@ def decryptBook(userkey, inpath, outpath):
return 1
if len(bookkey) != 64:
# Normal Adobe ADEPT
# Normal or "hardened" Adobe ADEPT
rsakey = RSA.import_key(userkey) # parses the ASN1 structure
bookkey = base64.b64decode(bookkey)
if int(keytype, 10) > 2:
bookkey = removeHardening(rights, keytype, bookkey)
try:
bookkey = PKCS1_v1_5.new(rsakey).decrypt(bookkey, None) # automatically unpads
except ValueError:

View File

@@ -49,13 +49,14 @@
# 9.0.0 - Add Python 3 compatibility for calibre 5
# 9.1.0 - Support for decrypting with owner password, support for V=5, R=5 and R=6 PDF files, support for AES256-encrypted PDFs.
# 9.1.1 - Only support PyCryptodome; clean up the code
# 10.0.0 - Add support for "hardened" Adobe DRM (RMSDK >= 10)
"""
Decrypts Adobe ADEPT-encrypted PDF files.
"""
__license__ = 'GPL v3'
__version__ = "9.1.1"
__version__ = "10.0.0"
import codecs
import hashlib
@@ -69,6 +70,7 @@ from decimal import Decimal
import itertools
import xml.etree.ElementTree as etree
import traceback
from uuid import UUID
try:
from Cryptodome.Cipher import AES, ARC4, PKCS1_v1_5
@@ -1633,6 +1635,24 @@ class PDFDocument(object):
self.ready = True
return
@staticmethod
def removeHardening(rights, keytype, keydata):
adept = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
textGetter = lambda name: ''.join(rights.findtext('.//%s' % (adept(name),)))
# Gather what we need, and generate the IV
resourceuuid = UUID(textGetter("resource"))
deviceuuid = UUID(textGetter("device"))
fullfillmentuuid = UUID(textGetter("fulfillment")[:36])
kekiv = UUID(int=resourceuuid.int ^ deviceuuid.int ^ fullfillmentuuid.int).bytes
# Derive kek from just "keytype"
rem = int(keytype, 10) % 16
H = SHA256(keytype.encode("ascii"))
kek = H[2*rem : 16 + rem] + H[rem : 2*rem]
return unpad(AES.new(kek, AES.MODE_CBC, kekiv).decrypt(keydata), 16)
def initialize_ebx_inept(self, password, docid, param):
self.is_printable = self.is_modifiable = self.is_extractable = True
rsakey = RSA.import_key(password) # parses the ASN1 structure
@@ -1641,16 +1661,12 @@ class PDFDocument(object):
rights = zlib.decompress(rights, -15)
rights = etree.fromstring(rights)
expr = './/{http://ns.adobe.com/adept}encryptedKey'
bookkey = ''.join(rights.findtext(expr))
bookkeyelem = rights.find(expr)
bookkey = codecs.decode(bookkeyelem.text.encode('utf-8'),'base64')
keytype = bookkeyelem.attrib.get('keyType', '0')
if len(bookkey) == 192:
print("This seems to be an Adobe ADEPT PDF with Adobe's new DRM")
print("This DRM cannot be removed yet. ")
print("Try getting your distributor to give you a new ACSM file, then open that in an old version of ADE (2.0).")
print("If your book distributor is not enforcing the new DRM yet, this will give you a copy with the old DRM.")
raise ADEPTNewVersionError("Book uses new ADEPT encryption")
bookkey = codecs.decode(bookkey.encode('utf-8'),'base64')
if int(keytype, 10) > 2:
bookkey = PDFDocument.removeHardening(rights, keytype, bookkey)
try:
bookkey = PKCS1_v1_5.new(rsakey).decrypt(bookkey, None) # automatically unpads
except ValueError: