acsm-calibre-plugin/calibre-plugin/getEncryptionKeyLinux.py
2022-01-16 19:00:29 +01:00

404 lines
13 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#@@CALIBRE_COMPAT_CODE@@
import sys, binascii
def unfuck(user):
# Wine uses a pretty nonstandard encoding in their registry file.
# I haven't found any existing Python implementation for that,
# so I looked at the C code and wrote my own.
# This implementation doesn't support multi-byte UTF-8 chars,
# but a standard-conforming Wine registry won't contain
# these anyways, so who cares.
hex_char_list = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 97, 98, 99, 100, 101, 102]
# Remove the quotation marks at beginning and end:
user = user.strip()[1:-1]
user_new = bytearray()
i = 0
while i < len(user):
# Convert string of len 1 to a byte
char = user[i][0].encode("latin-1")[0]
if sys.version_info[0] == 2:
char = ord(char)
if char == ord('\\'):
# Get next char:
i += 1
char = user[i][0].encode("latin-1")[0]
if sys.version_info[0] == 2:
char = ord(char)
if (char == ord('a')):
user_new.append(0x07)
elif (char == ord('b')):
user_new.append(0x08)
elif (char == ord('e')):
user_new.append(0x1b)
elif (char == ord('f')):
user_new.append(0x0c)
elif (char == ord('n')):
user_new.append(0x0a)
elif (char == ord('r')):
user_new.append(0x0d)
elif (char == ord('t')):
user_new.append(0x09)
elif (char == ord('v')):
user_new.append(0x0b)
elif (char == ord('x')):
# Get next char
i += 1
char = user[i][0].encode("latin-1")[0]
if sys.version_info[0] == 2:
char = ord(char)
if char not in hex_char_list:
user_new.append(ord('x'))
# This seems to be fallback code.
# Subtract 1 so the next char (the one that's not a hex char)
# is handled normally.
i -= 1
else:
ival = "" + chr(char)
# Read up to 3 more chars
next = user[i + 1][0].encode("latin-1")[0]
if sys.version_info[0] == 2:
next = ord(next)
if next in hex_char_list:
ival += chr(next)
i += 1
next = user[i + 1][0].encode("latin-1")[0]
if sys.version_info[0] == 2:
next = ord(next)
if next in hex_char_list:
ival += chr(next)
i += 1
next = user[i + 1][0].encode("latin-1")[0]
if sys.version_info[0] == 2:
next = ord(next)
if next in hex_char_list:
ival += chr(next)
i += 1
# ival now contains "00e9". Convert to int ...
ival = int(ival, 16)
# then drop everything except the lowest byte
ival = ival & 0xFF
# then add it to the array
user_new.append(ival)
elif (char >= ord('0') and char <= ord('9') ):
octal = char - ord('0')
# Read up to 2 more chars
next = user[i + 1][0].encode("latin-1")[0]
if sys.version_info[0] == 2:
next = ord(next)
if next >= ord('0') and next <= ord('9'):
octal = (octal * 8) + (next - ord('0'))
i += 1
next = user[i + 1][0].encode("latin-1")[0]
if sys.version_info[0] == 2:
next = ord(next)
if next >= ord('0') and next <= ord('9'):
octal = (octal * 8) + (next - ord('0'))
i += 1
else:
if (char < 0x80):
user_new.append(char)
else:
print("Multi-Byte UTF-8, not supported")
print("This should never happen in a standard-conform Wine registry ...")
return False
# Parse next char
i += 1
return user_new
def GetMasterKey(path_to_wine_prefix):
import os
if os.name == 'nt':
print("Hey! This is for Linux!")
return
verbose_logging = False
try:
import calibre_plugins.deacsm.prefs as prefs
deacsmprefs = prefs.DeACSM_Prefs()
verbose_logging = deacsmprefs["detailed_logging"]
except:
pass
import cpuid
import struct
try:
# Linux / Wine code just assumes that the system drive is C:\
serial_file = open(os.path.join(path_to_wine_prefix, "drive_c", ".windows-serial"), "r")
serial = serial_file.read()
serial_file.close()
serial = int(serial, 16)
except:
# 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
if (verbose_logging):
print("Serial: " + str(serial))
cpu = cpuid.CPUID()
_, b, c, d = cpu(0)
vendor = struct.pack("III", b, d, c)
if (verbose_logging):
print("Vendor: " + vendor.decode("utf-8"))
signature, _, _, _ = cpu(1)
signature = struct.pack('>I', signature)[1:]
if (verbose_logging):
print("Signature: " + str(binascii.hexlify(signature)))
# Search for the username in the registry:
user = None
# Linux - loop through the Wine registry file to find the "username" attribute
try:
registry_file = open(os.path.join(path_to_wine_prefix, "user.reg"))
waiting_for_username = False
while True:
line = registry_file.readline()
if not line:
break
if waiting_for_username:
if (not line.lower().startswith("\"username\"=")):
continue
# If we end up here, we have the username.
user = line.split('=', 1)[1].strip()
user = unfuck(user)
break
else:
if (line.startswith("[Software\\\\Adobe\\\\Adept\\\\Device]")):
waiting_for_username = True
if (line.startswith("[Volatile Environment]")):
waiting_for_username = True
registry_file.close()
except:
# There was an error hunting through the registry.
raise
pass
if (user is None):
print("Error while determining username ...")
exit()
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:
registry_file = open(os.path.join(path_to_wine_prefix, "user.reg"))
waiting_for_key = False
key_line = None
while True:
line = registry_file.readline()
if not line:
break
if waiting_for_key:
if (not line.lower().startswith("\"key\"=")):
continue
# If we end up here, we have the key.
key_line = line
while (key_line.strip().endswith('\\')):
key_line = key_line.strip()[:-1] + registry_file.readline()
# Now parse ...
key_line = key_line.split(':', 1)[1]
key_line = key_line.replace('\t', '').replace('\r', '').replace('\n', '').replace(' ', '').replace(',', '')
key_line = binascii.unhexlify(key_line)
else:
if (line.startswith("[Software\\\\Adobe\\\\Adept\\\\Device]")):
waiting_for_key = True
registry_file.close()
except:
# There was an error hunting through the registry.
raise
pass
if key_line is None:
print("No ADE activation found ...")
return None
if verbose_logging:
print("Encrypted key: " + binascii.hexlify(key_line))
# These should all be "bytes" (Py3) or "str" (Py2)
#print(type(vendor))
#print(type(signature))
#print(type(user))
if sys.version_info[0] == 2:
user = bytes(user)
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
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.
#
# 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
if verbose_logging:
print("Key key: ")
print(binascii.hexlify(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 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):
print("Wineprefix not found!!")
return None
# Default to win32 binary, unless we find arch in registry
winearch = "win32"
try:
system_registry_path = os.path.join(wineprefix, "system.reg")
regfile = open(system_registry_path, "r")
while True:
line = regfile.readline()
if not line:
break
stuff = re.match(r'#arch=(win32|win64)', line)
if (stuff):
winearch = stuff.groups()[0]
break
regfile.close()
except:
pass
# Execute!
env_dict = os.environ
env_dict["PYTHONPATH"] = ""
env_dict["WINEPREFIX"] = wineprefix
#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"] = binascii.hexlify(data)
env_dict["X_DECRYPT_ENTROPY"] = binascii.hexlify(entropy)
try:
from calibre.utils.config import config_dir
pluginsdir = os.path.join(config_dir,"plugins")
maindir = os.path.join(pluginsdir,"DeACSM")
moddir = os.path.join(maindir,"modules")
except:
import os
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()
# calls decrypt_win32.exe or decrypt_win64.exe
if prog_output.decode("utf-8").startswith("PROGOUTPUT:0:"):
key_string = prog_output.decode("utf-8").split(':')[2]
if verbose_logging:
print("Successfully got encryption key from WINE: " + key_string)
#print("Debug log:")
#print(prog_stderr.decode("utf-8"))
else:
print("Successfully got encryption key from WINE.")
master_key = binascii.unhexlify(key_string)
return True, master_key
else:
print("Huh. That didn't work. ")
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
if verbose_logging:
print("Program reported: " + prog_output.decode("utf-8"))
print("Debug log: ")
print(prog_stderr.decode("utf-8"))
return False, err
if __name__ == "__main__":
print("Do not execute this directly!")
exit()