Beta 3: Support for importing from Wine

This commit is contained in:
Florian Bach 2021-11-25 09:15:37 +01:00
parent 4c3ee827f0
commit f7eb9e5d79
15 changed files with 509 additions and 337 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/calibre-plugin/*.zip
/calibre-plugin/keyextract/*.exe

View File

@ -7,7 +7,7 @@ It is a full Python reimplementation of libgourou by Grégory Soutadé (http://i
1. Download the plugin and import it into Calibre
2. Open the plugin settings, it should say "Not authorized for any ADE ID"
3. If you have ADE installed on your machine (Windows+Mac only, no Linux/Wine), there will be a button "Import activation from ADE". Clicking that will automatically copy your account information from ADE over to the Calibre plugin without using up an activation.
3. If you have ADE installed on your machine, there will be a button "Import activation from ADE". Clicking that will automatically copy your account information from ADE over to the Calibre plugin without using up an activation.
4. If you don't have ADE installed, or you want to authorize a different account, or the automatic retrieval from ADE failed, click the "Link to ADE account" button to make a new clean authorization. You will then be asked to enter your AdobeID and password and to select an ADE version (ADE 2.0.1 recommended). A couple seconds later a success message should be displayed.
5. The settings window should now say "Authorized with ADE ID X on device Y, emulating ADE version Z".
6. Click the "Export account activation data" and "Export account encryption key" buttons to export / backup your keys. Do not skip this step. The first file (ZIP) can be used to re-authorize Calibre after a reset / reinstall without using up one of your Adobe authorizations. The second file (DER) can be imported into DeDRM.
@ -45,4 +45,5 @@ There's a bunch of features that could still be added, but most of them aren't i
- Support for anonymous Adobe IDs
- Support for un-authorizing a machine
- Support to copy an authorization from the plugin to an ADE install
- ...

View File

@ -7,7 +7,7 @@
[ ! -f calibre-plugin/pyasn1.zip ] && ./package_modules.sh
pushd calibre-plugin
pushd key-wine
pushd keyextract
# Compile:
make

View File

@ -20,9 +20,11 @@
# fix bug that would block other FileTypePlugins
# v0.0.12: Fix Calibre Plugin index / updater
# v0.0.13: Add support for emulating multiple ADE versions (1.7.2, 2.0.1, 3.0.1, 4.0.3, 4.5.11),
# add code to import existing activation from ADE (Windows+Mac only),
# add code to import existing activation from ADE (Windows, MacOS or Linux/Wine),
# add code to remove an existing activation from the plugin (Ctrl+Shift+D),
# fix race condition when importing multiple ACSMs simultaneously,
# fix authorization failing with certain non-ASCII characters in username.
# fix authorization failing with certain non-ASCII characters in username,
# add detailed logging toggle setting.
PLUGIN_NAME = "DeACSM"
PLUGIN_VERSION_TUPLE = (0, 0, 13)
@ -33,6 +35,7 @@ __version__ = PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE])
from calibre.utils.config import config_dir # type: ignore
from calibre.constants import isosx, iswindows, islinux # type: ignore
import os, shutil, traceback, sys, time, io
import zipfile
@ -100,6 +103,16 @@ class DeACSM(FileTypePlugin):
traceback.print_exc()
pass
if islinux:
# Also extract EXE files needed for WINE ADE key extraction
names = [ "keyextract/decrypt_win32.exe", "keyextract/decrypt_win64.exe" ]
lib_dict = self.load_resources(names)
for entry, data in lib_dict.items():
file_path = os.path.join(self.moddir, entry.split('/')[1])
f = open(file_path, "wb")
f.write(data)
f.close()
sys.path.insert(0, os.path.join(self.moddir, "cryptography"))
sys.path.insert(0, os.path.join(self.moddir, "rsa"))
sys.path.insert(0, os.path.join(self.moddir, "oscrypto"))

View File

@ -4,6 +4,7 @@
# pyright: reportUndefinedVariable=false
import os, base64, traceback
from PyQt5.QtGui import QKeySequence
from lxml import etree
@ -13,6 +14,7 @@ import time, datetime
from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
QGroupBox, QPushButton, QListWidget, QListWidgetItem, QInputDialog,
QLineEdit, QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl)
from PyQt5.QtWidgets import QShortcut
from PyQt5 import QtCore
@ -25,7 +27,7 @@ from calibre.gui2 import (question_dialog, error_dialog, info_dialog, choose_sav
from calibre_plugins.deacsm.__init__ import PLUGIN_NAME, PLUGIN_VERSION # type: ignore
import calibre_plugins.deacsm.prefs as prefs # type: ignore
from calibre.utils.config import config_dir # type: ignore
from calibre.constants import isosx, iswindows # type: ignore
from calibre.constants import isosx, iswindows, islinux # type: ignore
class ConfigWidget(QWidget):
@ -42,6 +44,7 @@ class ConfigWidget(QWidget):
self.tempdeacsmprefs['path_to_account_data'] = self.deacsmprefs['path_to_account_data']
self.tempdeacsmprefs['notify_fulfillment'] = self.deacsmprefs['notify_fulfillment']
self.tempdeacsmprefs['detailed_logging'] = self.deacsmprefs['detailed_logging']
self.tempdeacsmprefs['list_of_rented_books'] = self.deacsmprefs['list_of_rented_books']
@ -80,6 +83,12 @@ class ConfigWidget(QWidget):
self.button_import_WinADE.clicked.connect(self.import_activation_from_Win)
ua_group_box_layout.addWidget(self.button_import_WinADE)
if islinux:
self.button_import_LinuxWineADE = QtGui.QPushButton(self)
self.button_import_LinuxWineADE.setText(_("Import activation from ADE (Wine)"))
self.button_import_LinuxWineADE.clicked.connect(self.import_activation_from_LinuxWine)
ua_group_box_layout.addWidget(self.button_import_LinuxWineADE)
self.button_import_activation = QtGui.QPushButton(self)
self.button_import_activation.setText(_("Import existing activation backup (ZIP)"))
self.button_import_activation.clicked.connect(self.import_activation_from_ZIP)
@ -119,6 +128,17 @@ class ConfigWidget(QWidget):
self.chkNotifyFulfillment.setChecked(self.tempdeacsmprefs["notify_fulfillment"])
layout.addWidget(self.chkNotifyFulfillment)
self.chkDetailedLogging = QtGui.QCheckBox("Enable detailed debug logging")
self.chkDetailedLogging.setToolTip("Default: False\n\nIf this is enabled, the plugin debug logs will be more verbose which might be helpful in case of errors.\nHowever, it will also mean that private data like encryption keys or account credentials might end up in the logfiles.")
self.chkDetailedLogging.setChecked(self.tempdeacsmprefs["detailed_logging"])
self.chkDetailedLogging.toggled.connect(self.toggle_logging)
layout.addWidget(self.chkDetailedLogging)
# Key shortcut Ctrl+Shift+D to remove authorization, just like in ADE.
self.deauthShortcut = QShortcut(QKeySequence("Ctrl+Shift+D"), self)
self.deauthShortcut.activated.connect(self.delete_ade_auth)
try:
from calibre_plugins.deacsm.libadobe import createDeviceKeyFile, update_account_path, are_ade_version_lists_valid
@ -135,25 +155,104 @@ class ConfigWidget(QWidget):
update_account_path(self.deacsmprefs["path_to_account_data"])
self.resize(self.sizeHint())
if not are_ade_version_lists_valid():
# Internal error, this should never happen
if not activated:
self.button_link_account.setEnabled(False)
self.button_import_activation.setEnabled(False)
if isosx:
self.button_import_MacADE.setEnabled(activated)
if iswindows:
self.button_import_WinADE.setEnabled(activated)
else:
self.button_switch_ade_version.setEnabled(False)
self.button_export_key.setEnabled(False)
self.button_export_activation.setEnabled(False)
self.button_rented_books.setEnabled(False)
self.chkNotifyFulfillment.setEnabled(False)
try:
# Someone reported getting this error after upgrading the plugin.
# No idea why that happens - put a try/catch around just to be safe.
if not are_ade_version_lists_valid():
# Internal error, this should never happen
if not activated:
self.button_link_account.setEnabled(False)
self.button_import_activation.setEnabled(False)
if isosx:
self.button_import_MacADE.setEnabled(activated)
if iswindows:
self.button_import_WinADE.setEnabled(activated)
if islinux:
self.button_import_LinuxWineADE.setEnabled(activated)
else:
self.button_switch_ade_version.setEnabled(False)
self.button_export_key.setEnabled(False)
self.button_export_activation.setEnabled(False)
self.button_rented_books.setEnabled(False)
self.chkNotifyFulfillment.setEnabled(False)
error_dialog(None, "Internal error", "Version list mismatch. Please open a bug report.", show=True, show_copy_button=False)
error_dialog(None, "Internal error", "Version list mismatch. Please open a bug report.", show=True, show_copy_button=False)
except UnboundLocalError:
print("Verify function are_ade_version_lists_valid() not found - why?")
def toggle_logging(self):
if not self.chkDetailedLogging.isChecked():
return
msg = "You have enabled detailed logging.\n"
msg += "This will cause various data to be included in the logfiles, like encryption keys, account keys and other confidential data.\n"
msg += "With this setting enabled, only share log files privately with the developer and don't make them publicly available."
info_dialog(None, "Warning", msg, show=True, show_copy_button=False)
def delete_ade_auth(self):
# This function can only be triggered with the key combination Ctrl+Shift+D.
# There is no easy-to-access button to trigger that to prevent people from
# accidentally deleting their authorization.
info_string, activated, ade_mail = self.get_account_info()
if not activated:
# If there is no authorization, there's nothing to delete
return
msg = "Are you sure you want to remove the ADE authorization?\n"
if ade_mail is None:
msg += "The current authorization is an anonymous login. It will be permanently lost if you proceed.\n\n"
else:
msg += "You will use up one of your six activations if you want to authorize your account again in the future.\n\n"
msg += "Click 'Yes' to delete the authorization or 'No' to cancel."
ok = question_dialog(None, "Remove ADE account", msg)
if (not ok):
return
msg = "Do you want to create a backup of the current authorization?\n"
msg += "This backup can be imported again without using up one of your authorizations.\n\n"
msg += "Click 'Yes' to create a backup before deleting, click 'No' to delete without backup."
ok = question_dialog(None, "Remove ADE account", msg)
if (ok):
# Create a backup:
backup_success = self.export_activation()
if (not backup_success):
error_dialog(None, "Export failed", "The backup was unsuccessful - authorization will not be deleted.", show=True, show_copy_button=False)
return
# Okay, once we are here, we can be pretty sure the user actually wants to delete their authorization.
try:
os.remove(os.path.join(self.deacsmprefs["path_to_account_data"], "activation.xml"))
os.remove(os.path.join(self.deacsmprefs["path_to_account_data"], "device.xml"))
os.remove(os.path.join(self.deacsmprefs["path_to_account_data"], "devicesalt"))
except:
error_dialog(None, "Remove ADE account", "There was an error while removing the authorization.", show=True, show_copy_button=False)
# Show success, then close:
info_dialog(None, "Remove ADE account", "ADE authorization successfully removed.", show=True, show_copy_button=False)
try:
self.button_switch_ade_version.setEnabled(False)
except:
pass
self.button_export_activation.setEnabled(False)
self.button_export_key.setEnabled(False)
self.lblAccInfo.setText("Authorization deleted.\nClose and re-open this window to add a new authorization.")
def get_account_info(self):
@ -236,6 +335,7 @@ class ConfigWidget(QWidget):
except:
print("{0} v{1}: Error while importing Account stuff".format(PLUGIN_NAME, PLUGIN_VERSION))
traceback.print_exc()
return False
update_account_path(self.deacsmprefs["path_to_account_data"])
@ -253,7 +353,7 @@ class ConfigWidget(QWidget):
filters, all_files=False, initial_filename=export_filename)
if (filename is None):
return
return False
print("{0} v{1}: Exporting activation data to {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename))
@ -262,8 +362,97 @@ class ConfigWidget(QWidget):
zipfile.write(os.path.join(self.deacsmprefs["path_to_account_data"], "device.xml"), "device.xml")
zipfile.write(os.path.join(self.deacsmprefs["path_to_account_data"], "activation.xml"), "activation.xml")
zipfile.write(os.path.join(self.deacsmprefs["path_to_account_data"], "devicesalt"), "devicesalt")
return True
except:
return error_dialog(None, "Export failed", "Export failed.", show=True, show_copy_button=False)
error_dialog(None, "Export failed", "Export failed.", show=True, show_copy_button=False)
return False
def check_ADE_registry(self, wineprefix):
# Gets a path to a WINEPREFIX and returns True if this is useable.
# Checks if the Wine registry contains an ADE activation.
try:
registry_file = open(os.path.join(wineprefix, "user.reg"))
while True:
line = registry_file.readline()
if not line:
break
if line.strip().startswith("[Software\\\\Adobe\\\\Adept\\\\Activation\\\\0000"):
return True
except:
print("Exception while validating WINEPREFIX:")
print(traceback.format_exc())
return False
def import_activation_from_LinuxWine(self):
# This will try to import the activation from Adobe Digital Editions on Linux / Wine ...
msg = "Trying to import existing activation from Adobe Digital Editions in WINE ...\n"
msg += "Note: Importing the activation can take up to 30 seconds, and Calibre will appear to be \"stuck\" during that time.\n\n"
msg += "Please enter the full, absolute path to your WINEPREFIX."
msg += "If there's already a path in the input box, it is usually (but not always) the correct one."
default_path = ""
if (default_path == ""):
# Check WINEPREFIX env variable
env_wineprefix = os.getenv("WINEPREFIX", None)
if (env_wineprefix is not None and os.path.isdir(env_wineprefix)):
if self.check_ADE_registry(env_wineprefix):
default_path = env_wineprefix
if (default_path == ""):
# Use default path ".wine" in HOME dir
home_wineprefix = os.path.join(os.path.expanduser("~"), ".wine")
if (os.path.isdir(home_wineprefix)):
if self.check_ADE_registry(home_wineprefix):
default_path = home_wineprefix
text, ok = QInputDialog.getText(self, "Importing authorization", msg, text=default_path)
if (not ok):
return
if (not os.path.isdir(text)):
return error_dialog(None, "Import failed", "The WINEPREFIX path you entered doesn't seem to exist.", show=True, show_copy_button=False)
if (not self.check_ADE_registry(text)):
return error_dialog(None, "Import failed", "The WINEPREFIX you entered doesn't seem to contain an authorized ADE.", show=True, show_copy_button=False)
from calibre_plugins.deacsm.libadobeImportAccount import importADEactivationLinuxWine
ret, msg = importADEactivationLinuxWine(text)
if (ret):
# update display
info_string, activated, ade_mail = self.get_account_info()
self.lblAccInfo.setText(info_string)
self.button_link_account.setEnabled(not activated)
self.button_import_activation.setEnabled(not activated)
self.button_import_LinuxWineADE.setEnabled(not activated)
self.button_export_key.setEnabled(activated)
self.button_export_activation.setEnabled(activated)
self.resize(self.sizeHint())
if (activated):
if ade_mail is None:
info_dialog(None, "Done", "Successfully imported an anonymous authorization", show=True, show_copy_button=False)
else:
info_dialog(None, "Done", "Successfully imported authorization for " + ade_mail, show=True, show_copy_button=False)
else:
error_dialog(None, "Import failed", "Import looks like it worked, but the resulting files seem to be corrupted ...", show=True, show_copy_button=False)
else:
error_dialog(None, "Import failed", "That didn't work:\n" + msg, show=True, show_copy_button=False)
def import_activation_from_Win(self):
# This will try to import the activation from Adobe Digital Editions on Windows ...
@ -382,16 +571,14 @@ class ConfigWidget(QWidget):
self.button_import_activation.setEnabled(not activated)
self.button_export_key.setEnabled(activated)
self.button_export_activation.setEnabled(activated)
try:
self.button_import_MacADE.setEnabled(activated)
except:
pass
if isosx:
self.button_import_MacADE.setEnabled(not activated)
if iswindows:
self.button_import_WinADE.setEnabled(not activated)
if islinux:
self.button_import_LinuxWineADE.setEnabled(not activated)
try:
self.button_import_WinADE.setEnabled(activated)
except:
pass
self.resize(self.sizeHint())
if ade_mail is None:
@ -497,7 +684,7 @@ class ConfigWidget(QWidget):
return error_dialog(None, "Failed", "Error while changing ADE version: " + msg, show=True, show_copy_button=False)
except:
return error_dialog(None, "Failed", "Error while changing ADE version.", show=True, det_msg=err, show_copy_button=False)
return error_dialog(None, "Failed", "Error while changing ADE version.", show=True, det_msg=traceback.format_exc(), show_copy_button=False)
def link_account(self):
@ -583,14 +770,12 @@ class ConfigWidget(QWidget):
self.button_import_activation.setEnabled(False)
self.button_export_key.setEnabled(True)
self.button_export_activation.setEnabled(True)
try:
if isosx:
self.button_import_MacADE.setEnabled(False)
except:
pass
try:
if iswindows:
self.button_import_WinADE.setEnabled(False)
except:
pass
if islinux:
self.button_import_LinuxWineADE.setEnabled(False)
self.resize(self.sizeHint())
@ -644,6 +829,7 @@ class ConfigWidget(QWidget):
def save_settings(self):
self.deacsmprefs.set('notify_fulfillment', self.chkNotifyFulfillment.isChecked())
self.deacsmprefs.set('detailed_logging', self.chkDetailedLogging.isChecked())
self.deacsmprefs.writeprefs()
def load_resource(self, name):

View File

@ -2,6 +2,9 @@
# -*- coding: utf-8 -*-
from re import VERBOSE
def unfuck(user):
# Wine uses a pretty nonstandard encoding in their registry file.
# I haven't found any existing Python implementation for that,
@ -109,7 +112,19 @@ def GetMasterKey(path_to_wine_prefix):
print("Hey! This is for Linux!")
return
import cpuid
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass
try:
import cpuid
except:
import calibre_plugins.deacsm.cpuid as cpuid
import struct
try:
@ -119,21 +134,26 @@ def GetMasterKey(path_to_wine_prefix):
serial_file.close()
serial = int(serial, 16)
except:
# If this file is not present, Wine will use a default serial number of "0".
# If this file is not present, Wine will usually use a default serial number of "0".
# There are some edge cases where Wine uses a different serial number even when that
# .windows-serial file is not present.
serial = 0
print("Serial: " + str(serial))
if (verbose_logging):
print("Serial: " + str(serial))
cpu = cpuid.CPUID()
_, b, c, d = cpu(0)
vendor = struct.pack("III", b, d, c)
print("Vendor: " + vendor.decode("utf-8"))
if (verbose_logging):
print("Vendor: " + vendor.decode("utf-8"))
signature, _, _, _ = cpu(1)
signature = struct.pack('>I', signature)[1:]
print("Signature: " + str(signature.hex()))
if (verbose_logging):
print("Signature: " + str(signature.hex()))
# Search for the username in the registry:
user = None
@ -174,7 +194,8 @@ def GetMasterKey(path_to_wine_prefix):
print("Error while determining username ...")
exit()
print("Username: " + str(user))
if verbose_logging:
print("Username: " + str(user))
# Find the value we want to decrypt from the registry. loop through the Wine registry file to find the "key" attribute
try:
@ -213,12 +234,12 @@ def GetMasterKey(path_to_wine_prefix):
raise
pass
if key_line is None:
print("No ADE activation found ...")
return None
print("Encrypted key: " + str(key_line))
import hexdump
hexdump.hexdump(key_line)
if verbose_logging:
print("Encrypted key: " + str(key_line))
# These should all be "bytes" or "bytearray"
#print(type(vendor))
@ -227,29 +248,47 @@ def GetMasterKey(path_to_wine_prefix):
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
print("Entropy: " + str(entropy))
if verbose_logging:
print("Entropy: " + str(entropy))
# We would now call CryptUnprotectData to decrypt the stuff,
# but unfortunately there's no working Linux implementation
# for that. This means we have to call a Windows binary through
# for that.
#
# The plan was to handle everything in Python so we don't have
# to interact with Wine - that's why we're doing all the registry
# handling ourselves.
# Unfortunately, that doesn't work for the actual decryption.
#
# This means we have to call a Windows binary through
# Wine just for this one single decryption call ...
success, data = CryptUnprotectDataExecuteWine(path_to_wine_prefix, key_line, entropy)
if (success):
keykey = data
print(keykey)
if verbose_logging:
print("Key key: ")
print(keykey)
return keykey
else:
print("Error number: " + str(data))
if data == 13: # WINError ERROR_INVALID_DATA
print("Could not decrypt data with the given key. Did the Wine username change?")
print("Could not decrypt data with the given key. Did the entropy change?")
return None
def CryptUnprotectDataExecuteWine(wineprefix, data, entropy):
import subprocess, os, re
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass
print("Asking WINE to decrypt encrypted key for us ...")
if wineprefix == "" or not os.path.exists(wineprefix):
@ -282,10 +321,10 @@ def CryptUnprotectDataExecuteWine(wineprefix, data, entropy):
env_dict = os.environ
env_dict["PYTHONPATH"] = ""
env_dict["WINEPREFIX"] = wineprefix
env_dict["WINEDEBUG"] = "-all,+crypt"
import base64
#env_dict["WINEDEBUG"] = "-all,+crypt"
env_dict["WINEDEBUG"] = "+err,+fixme"
# Use environment variables to get the input data to the application.
env_dict["X_DECRYPT_DATA"] = data.hex()
env_dict["X_DECRYPT_ENTROPY"] = entropy.hex()
@ -296,7 +335,7 @@ def CryptUnprotectDataExecuteWine(wineprefix, data, entropy):
moddir = os.path.join(maindir,"modules")
except:
import os
moddir = os.path.dirname(os.path.abspath(__file__))
moddir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "keyextract")
proc = subprocess.Popen(["wine", "decrypt_" + winearch + ".exe" ], shell=False, cwd=moddir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
prog_output, prog_stderr = proc.communicate()
@ -305,22 +344,34 @@ def CryptUnprotectDataExecuteWine(wineprefix, data, entropy):
if prog_output.decode("utf-8").startswith("PROGOUTPUT:0:"):
key_string = prog_output.decode("utf-8").split(':')[2]
print("Successfully got encryption key from WINE: " + key_string)
if verbose_logging:
print("Successfully got encryption key from WINE: " + key_string)
else:
print("Successfully got encryption key from WINE.")
master_key = bytes.fromhex(key_string)
return True, master_key
else:
print("Huh. That didn't work. ")
err = int(prog_output.decode("utf-8").split(':')[1])
if err == -4:
err = int(prog_output.decode("utf-8").split(':')[2])
try:
err = int(prog_output.decode("utf-8").split(':')[1])
if err == -4:
err = int(prog_output.decode("utf-8").split(':')[2])
new_serial = int(prog_output.decode("utf-8").split(':')[3])
if verbose_logging:
print("New serial: " + str(new_serial))
except:
pass
print("Program reported: " + prog_output.decode("utf-8"))
print("Debug log: ")
print(prog_stderr.decode("utf-8"))
if verbose_logging:
print("Program reported: " + prog_output.decode("utf-8"))
print("Debug log: ")
print(prog_stderr.decode("utf-8"))
return False, err
GetMasterKey()
if __name__ == "__main__":
print("Do not execute this directly!")
exit()

View File

@ -25,14 +25,13 @@ def GetSystemDirectory():
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
GetSystemDirectoryW.restype = c_uint
def GetSystemDirectory():
buffer = create_unicode_buffer(MAX_PATH + 1)
GetSystemDirectoryW(buffer, len(buffer))
return buffer.value
return GetSystemDirectory
GetSystemDirectory = GetSystemDirectory()
def GetVolumeSerialNumber():
buffer = create_unicode_buffer(MAX_PATH + 1)
GetSystemDirectoryW(buffer, len(buffer))
return buffer.value
def GetVolumeSerialNumber(path):
from ctypes import windll, c_wchar_p, c_uint, POINTER, byref
kernel32 = windll.kernel32
@ -41,13 +40,11 @@ def GetVolumeSerialNumber():
POINTER(c_uint), POINTER(c_uint),
POINTER(c_uint), c_wchar_p, c_uint]
GetVolumeInformationW.restype = c_uint
def GetVolumeSerialNumber(path):
vsn = c_uint(0)
GetVolumeInformationW(
path, None, 0, byref(vsn), None, None, None, 0)
return vsn.value
return GetVolumeSerialNumber
GetVolumeSerialNumber = GetVolumeSerialNumber()
vsn = c_uint(0)
GetVolumeInformationW(
path, None, 0, byref(vsn), None, None, None, 0)
return vsn.value
def GetUserNameWINAPI():
@ -113,23 +110,40 @@ def GetMasterKey():
if os.name != 'nt':
print("This script is for Windows!")
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass
# Get serial number of root drive
root = GetSystemDirectory().split('\\')[0] + '\\'
serial = GetVolumeSerialNumber(root)
print("Serial: " + str(serial))
if verbose_logging:
print("Serial: " + str(serial))
# Get CPU vendor:
import cpuid, struct
try:
import cpuid
except:
import calibre_plugins.deacsm.cpuid as cpuid
import struct
cpu = cpuid.CPUID()
_, b, c, d = cpu(0)
vendor = struct.pack("III", b, d, c)
print("Vendor: " + vendor.decode("utf-8"))
if verbose_logging:
print("Vendor: " + vendor.decode("utf-8"))
signature, _, _, _ = cpu(1)
signature = struct.pack('>I', signature)[1:]
print("Signature: " + str(signature.hex()))
if verbose_logging:
print("Signature: " + str(signature.hex()))
# Search for the username in the registry:
user = None
@ -143,12 +157,13 @@ def GetMasterKey():
else:
user = current_user_name
if (user_from_registry is not None and user_from_registry != current_user_name):
print("Username: {0}/{1} mismatch, using {0}".format(str(user_from_registry), str(current_user_name)))
elif (user_from_registry is not None):
print("Username: {0} (Registry)".format(str(user_from_registry)))
else:
print("Username: {0} (WinAPI)".format(str(current_user_name)))
if verbose_logging:
if (user_from_registry is not None and user_from_registry != current_user_name):
print("Username: {0}/{1} mismatch, using {0}".format(str(user_from_registry), str(current_user_name)))
elif (user_from_registry is not None):
print("Username: {0} (Registry)".format(str(user_from_registry)))
else:
print("Username: {0} (WinAPI)".format(str(current_user_name)))
@ -164,9 +179,10 @@ def GetMasterKey():
device = winreg.QueryValueEx(regkey, 'key')[0]
except:
print("Can't find encrypted device key.")
return None
print("Encrypted key: " + str(device))
if verbose_logging:
print("Encrypted key: " + str(device))
# These three must all be bytes.
#print(type(vendor))
@ -175,16 +191,19 @@ def GetMasterKey():
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
print("Entropy: " + str(entropy))
if verbose_logging:
print("Entropy: " + str(entropy))
keykey = CryptUnprotectData(device, entropy)
if (keykey is None):
print("Couldn't decrypt key!")
return None
print("Decrypted key: " + str(keykey))
if verbose_logging:
print("Decrypted key: " + str(keykey))
return keykey
GetMasterKey()
if __name__ == "__main__":
GetMasterKey()

View File

@ -4,6 +4,8 @@
#include <windows.h>
#include <wincrypt.h>
#include <dpapi.h>
#include <fileapi.h>
#include <direct.h>
#ifdef DEBUG
#undef DEBUG
@ -45,7 +47,7 @@ void hexDump (
// Output description if given.
if (desc != NULL) printf ("%s:\n", desc);
if (desc != NULL) fprintf (stderr, "%s:\n", desc);
// Length checks.
@ -66,7 +68,7 @@ void hexDump (
if ((i % perLine) == 0) {
// Only print previous-line ASCII buffer for lines beyond first.
if (i != 0) printf (" %s\n", buff);
if (i != 0) fprintf (stderr, " %s\n", buff);
// Output the offset of current line.
@ -99,6 +101,15 @@ void hexDump (
}
#endif
int get_serial() {
DWORD serial = 0;
int retval = GetVolumeInformation("c:\\\\", NULL, 0, &serial, NULL, NULL, NULL, 0);
if (retval == 0) {
fprintf(stderr, "Error with GetVolumeInformation: %d\n", GetLastError());
return 0;
}
return serial;
}
int main() {
char * var_data = "X_DECRYPT_DATA";
@ -164,7 +175,16 @@ if (ret) {
exit(0);
}
else {
printf("PROGOUTPUT:-4:%d", GetLastError());
// Apparently Wine has issues with the volume serial code sometimes
// so the code on the Linux side detects the wrong serial number.
// Thus, if the decryption fails, we read the serial number that Wine
// (and ADE) sees back to the Linux side for another attempt.
int err = GetLastError();
printf("PROGOUTPUT:-4:%d:%08x", err, get_serial());
exit(-4);
}

View File

@ -437,6 +437,14 @@ def activateDevice(useVersionIndex: int = 0):
# ADE 1.7.2 or another version that authorization is disabled for
return False, "Authorization not supported for this build ID"
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass
result, activate_req = buildActivateReq(useVersionIndex)
if (result is False):
@ -455,8 +463,9 @@ def activateDevice(useVersionIndex: int = 0):
etree.SubElement(req_xml, etree.QName(NSMAP["adept"], "signature")).text = signature
#print ("final request:")
#print(etree.tostring(req_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1"))
if verbose_logging:
print ("Activation request:")
print(etree.tostring(req_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1"))
data = "<?xml version=\"1.0\"?>\n" + etree.tostring(req_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1")
@ -487,8 +496,9 @@ def activateDevice(useVersionIndex: int = 0):
except:
return False, "Error parsing Adobe /Activate response"
#print("Response from server: ")
#print(ret)
if verbose_logging:
print("Response from server: ")
print(ret)
# Soooo, lets go and append that to the XML:

View File

@ -1,225 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Most of the code in this file has been taken from adobekey.pyw written by i♥cabbages
# adobekey.pyw, version 7.0
# Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
create_unicode_buffer, create_string_buffer, CFUNCTYPE, \
string_at, Structure, c_void_p, cast, c_size_t, memmove
from ctypes.wintypes import LPVOID, DWORD, BOOL
import struct
try:
import winreg
except ImportError:
import _winreg as winreg
MAX_PATH = 255
kernel32 = windll.kernel32
advapi32 = windll.advapi32
crypt32 = windll.crypt32
def GetSystemDirectory():
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
GetSystemDirectoryW.restype = c_uint
def GetSystemDirectory():
buffer = create_unicode_buffer(MAX_PATH + 1)
GetSystemDirectoryW(buffer, len(buffer))
return buffer.value
return GetSystemDirectory
GetSystemDirectory = GetSystemDirectory()
def GetVolumeSerialNumber():
GetVolumeInformationW = kernel32.GetVolumeInformationW
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
POINTER(c_uint), POINTER(c_uint),
POINTER(c_uint), c_wchar_p, c_uint]
GetVolumeInformationW.restype = c_uint
def GetVolumeSerialNumber(path):
vsn = c_uint(0)
GetVolumeInformationW(
path, None, 0, byref(vsn), None, None, None, 0)
return vsn.value
return GetVolumeSerialNumber
GetVolumeSerialNumber = GetVolumeSerialNumber()
def GetUserName():
GetUserNameW = advapi32.GetUserNameW
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
GetUserNameW.restype = c_uint
def GetUserName():
buffer = create_unicode_buffer(32)
size = c_uint(len(buffer))
while not GetUserNameW(buffer, byref(size)):
buffer = create_unicode_buffer(len(buffer) * 2)
size.value = len(buffer)
return buffer.value.encode('utf-16-le')[::2]
return GetUserName
GetUserName = GetUserName()
PAGE_EXECUTE_READWRITE = 0x40
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
def VirtualAlloc():
_VirtualAlloc = kernel32.VirtualAlloc
_VirtualAlloc.argtypes = [LPVOID, c_size_t, DWORD, DWORD]
_VirtualAlloc.restype = LPVOID
def VirtualAlloc(addr, size, alloctype=(MEM_COMMIT | MEM_RESERVE),
protect=PAGE_EXECUTE_READWRITE):
return _VirtualAlloc(addr, size, alloctype, protect)
return VirtualAlloc
VirtualAlloc = VirtualAlloc()
MEM_RELEASE = 0x8000
def VirtualFree():
_VirtualFree = kernel32.VirtualFree
_VirtualFree.argtypes = [LPVOID, c_size_t, DWORD]
_VirtualFree.restype = BOOL
def VirtualFree(addr, size=0, freetype=MEM_RELEASE):
return _VirtualFree(addr, size, freetype)
return VirtualFree
VirtualFree = VirtualFree()
class NativeFunction(object):
def __init__(self, restype, argtypes, insns):
self._buf = buf = VirtualAlloc(None, len(insns))
memmove(buf, insns, len(insns))
ftype = CFUNCTYPE(restype, *argtypes)
self._native = ftype(buf)
def __call__(self, *args):
return self._native(*args)
def __del__(self):
if self._buf is not None:
VirtualFree(self._buf)
self._buf = None
if struct.calcsize("P") == 4:
CPUID0_INSNS = (
b"\x53" # push %ebx
b"\x31\xc0" # xor %eax,%eax
b"\x0f\xa2" # cpuid
b"\x8b\x44\x24\x08" # mov 0x8(%esp),%eax
b"\x89\x18" # mov %ebx,0x0(%eax)
b"\x89\x50\x04" # mov %edx,0x4(%eax)
b"\x89\x48\x08" # mov %ecx,0x8(%eax)
b"\x5b" # pop %ebx
b"\xc3" # ret
)
CPUID1_INSNS = (
b"\x53" # push %ebx
b"\x31\xc0" # xor %eax,%eax
b"\x40" # inc %eax
b"\x0f\xa2" # cpuid
b"\x5b" # pop %ebx
b"\xc3" # ret
)
else:
CPUID0_INSNS = (
b"\x49\x89\xd8" # mov %rbx,%r8
b"\x49\x89\xc9" # mov %rcx,%r9
b"\x48\x31\xc0" # xor %rax,%rax
b"\x0f\xa2" # cpuid
b"\x4c\x89\xc8" # mov %r9,%rax
b"\x89\x18" # mov %ebx,0x0(%rax)
b"\x89\x50\x04" # mov %edx,0x4(%rax)
b"\x89\x48\x08" # mov %ecx,0x8(%rax)
b"\x4c\x89\xc3" # mov %r8,%rbx
b"\xc3" # retq
)
CPUID1_INSNS = (
b"\x53" # push %rbx
b"\x48\x31\xc0" # xor %rax,%rax
b"\x48\xff\xc0" # inc %rax
b"\x0f\xa2" # cpuid
b"\x5b" # pop %rbx
b"\xc3" # retq
)
def cpuid0():
_cpuid0 = NativeFunction(None, [c_char_p], CPUID0_INSNS)
buf = create_string_buffer(12)
def cpuid0():
_cpuid0(buf)
return buf.raw
return cpuid0
cpuid0 = cpuid0()
cpuid1 = NativeFunction(c_uint, [], CPUID1_INSNS)
class DataBlob(Structure):
_fields_ = [('cbData', c_uint),
('pbData', c_void_p)]
DataBlob_p = POINTER(DataBlob)
def CryptUnprotectData():
_CryptUnprotectData = crypt32.CryptUnprotectData
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
c_void_p, c_void_p, c_uint, DataBlob_p]
_CryptUnprotectData.restype = c_uint
def CryptUnprotectData(indata, entropy):
indatab = create_string_buffer(indata)
indata = DataBlob(len(indata), cast(indatab, c_void_p))
entropyb = create_string_buffer(entropy)
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
outdata = DataBlob()
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
None, None, 0, byref(outdata)):
raise Exception("Failed to decrypt user key key (sic)")
return string_at(outdata.pbData, outdata.cbData)
return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData()
DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device'
def GetMasterKey():
root = GetSystemDirectory().split('\\')[0] + '\\'
serial = GetVolumeSerialNumber(root)
vendor = cpuid0()
signature = struct.pack('>I', cpuid1())[1:]
try:
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, DEVICE_KEY_PATH)
device = winreg.QueryValueEx(regkey, 'key')[0]
# ADE puts an "username" attribute into that key which was unused
# in previous versions of this script. This means that this key
# retrieval script would break / not work if the user had ever
# changed their Windows account user name after installing ADE.
# By reading the "username" registry entry if available we won't
# have that problem anymore.
try:
user = winreg.QueryValueEx(regkey, 'username')[0].encode('utf-16-le')[::2]
# Yes, this actually only uses the lowest byte of each character.
except:
# This value should always be available, but just in case
# it's not, use the old implementation.
user = GetUserName()
except WindowsError:
return None
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
try:
keykey = CryptUnprotectData(device, entropy)
except Exception:
# There was an exception, so this thing was unable to decrypt
# the key. Maybe this is due to the new user name handling, so
# let's retry with the old code.
user = GetUserName()
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
keykey = CryptUnprotectData(device, entropy)
return keykey

View File

@ -26,7 +26,7 @@ def buildFulfillRequest(acsm):
fingerprint = None
device_type = None
fingerprint = activationxml.find("./%s/%s" % (adNS("activationToken"), adNS("fingerprint"))).text
fingerprint = activationxml.find("./%s/%s" % (adNS("activationToken"), adNS("deviceType"))).text
device_type = activationxml.find("./%s/%s" % (adNS("activationToken"), adNS("deviceType"))).text
except:
pass
@ -286,6 +286,15 @@ def buildRights(license_token_node):
def fulfill(acsm_file, do_notify = False):
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass
# Get pkcs12:
pkcs12 = None
@ -325,7 +334,9 @@ def fulfill(acsm_file, do_notify = False):
fulfill_request, adept_ns = buildFulfillRequest(acsmxml)
#print(fulfill_request)
if verbose_logging:
print("Fulfill request:")
print(fulfill_request)
fulfill_request_xml = etree.fromstring(fulfill_request)
# Sign the request:
@ -388,8 +399,9 @@ def fulfill(acsm_file, do_notify = False):
else:
return False, "Looks like there's been an error during Fulfillment: %s" % replyData
# Print fulfillmentResult
#print(replyData)
if verbose_logging:
print("fulfillmentResult:")
print(replyData)
adobe_fulfill_response = etree.fromstring(replyData)
NSMAP = { "adept" : "http://ns.adobe.com/adept" }

View File

@ -19,6 +19,88 @@ except:
from calibre_plugins.deacsm.libadobe import VAR_VER_HOBBES_VERSIONS, VAR_VER_OS_IDENTIFIERS, VAR_VER_DEFAULT_BUILD_ID, VAR_VER_BUILD_IDS
def importADEactivationLinuxWine(wine_prefix_path, buildIDtoEmulate=VAR_VER_DEFAULT_BUILD_ID):
# Similar to importADEactivationWindows - extracts the activation data from a Wine prefix
try:
from calibre.constants import islinux
if not islinux:
print("This function is for Linux only!")
return False, "Linux only!"
except:
pass
# Get encryption key
try:
from getEncryptionKeyLinux import GetMasterKey
except:
from calibre_plugins.deacsm.getEncryptionKeyLinux import GetMasterKey
master_key = GetMasterKey(wine_prefix_path)
if master_key is None:
err = "Could not access ADE encryption key. If you have just installed ADE in Wine, "
err += "please reboot your machine then try again. Also, make sure neither ADE nor any other "
err += "software is running in WINE while you're trying to import the authorization. "
err += "If it still doesn't work but ADE in that particular WINEPREFIX is working fine, "
err += "please open a bug report."
return False, err
# Loop through the registry:
try:
registry_file = open(os.path.join(wine_prefix_path, "user.reg"), "r")
waiting_for_element = False
current_parent = None
current_name = None
current_value = None
current_method = None
while True:
line = registry_file.readline()
if not line:
break
line = line.strip()
if waiting_for_element:
if (line.lower().startswith("@=")):
current_name = line.split('=', 1)[1].strip()[1:-1]
continue
if (line.lower().startswith('"value"=')):
current_value = line.split('=', 1)[1].strip()[1:-1]
continue
if (line.lower().startswith('"method"=')):
current_method = line.split('=', 1)[1].strip()[1:-1]
continue
if (len(line) == 0):
# Empty line - finalize this element
if current_value is None:
current_parent = current_name
current_name = None
current_method = None
current_value = None
waiting_for_element = False
continue
handle_subkey(current_parent, current_name, current_value, master_key, current_method, None)
current_name = None
current_value = None
current_method = None
else:
if (line.startswith("[Software\\\\Adobe\\\\Adept\\\\Activation\\\\")):
waiting_for_element = True
registry_file.close()
return handle_subkey(None, None, None, master_key, None, buildIDtoEmulate)
except:
# There was an error hunting through the registry.
raise
pass
def importADEactivationWindows(buildIDtoEmulate=VAR_VER_DEFAULT_BUILD_ID):
# Tries to import the system activation from Adobe Digital Editions on Windows into the plugin
# This can be used to "clone" the ADE activation so you don't need to waste an additional activation.
@ -35,14 +117,14 @@ def importADEactivationWindows(buildIDtoEmulate=VAR_VER_DEFAULT_BUILD_ID):
# Get encryption key:
try:
from libadobeEncryptionWindows import GetMasterKey
from getEncryptionKeyWindows import GetMasterKey
except:
from calibre_plugins.deacsm.libadobeEncryptionWindows import GetMasterKey
from calibre_plugins.deacsm.getEncryptionKeyWindows import GetMasterKey
master_key = GetMasterKey()
if master_key is None:
return False, "master_key is None ..."
return False, "Could not access ADE encryption key"
PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation'
@ -185,7 +267,7 @@ def handle_subkey(parent, subkey, value, encryption_key, method, buildID):
# The first 16 bytes of the fingerprint are used as IV for the privateLicenseKey
# Older versions of this decryption code, like in the DeDRM plugin, didn't
# do that correctly. For DeDRM that doesn't matter as a wrong IV only causes
# the first 16 bytes to be corrupted, and these aren't used for decryption anyways.
# the first 16 bytes to be corrupted, and these aren't used for eBook decryption anyways.
# For this plugin I want the exact correct data, so lets use the fingerprint as IV.
# See jhowell's post: https://www.mobileread.com/forums/showpost.php?p=4173908

View File

@ -19,6 +19,7 @@ class DeACSM_Prefs():
self.deacsmprefs.defaults['configured'] = False
self.deacsmprefs.defaults['notify_fulfillment'] = True
self.deacsmprefs.defaults['detailed_logging'] = False
self.deacsmprefs.defaults['list_of_rented_books'] = []