diff --git a/calibre-plugin/getEncryptionKeyLinux.py b/calibre-plugin/getEncryptionKeyLinux.py index de0e619..ee46351 100644 --- a/calibre-plugin/getEncryptionKeyLinux.py +++ b/calibre-plugin/getEncryptionKeyLinux.py @@ -5,307 +5,7 @@ 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: {}".format(serial)) - - cpu = cpuid.CPUID() - _, b, c, d = cpu(0) - vendor = struct.pack("III", b, d, c) - - if (verbose_logging): - print("Vendor: {}".format(vendor)) - - signature, _, _, _ = cpu(1) - signature = struct.pack('>I', signature)[1:] - - if (verbose_logging): - print("Signature: {}".format(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() - - # Comes as bytearray - if sys.version_info[0] == 3: - user = bytes(user) - else: - user = str(user) - - if verbose_logging: - print("Username: {}".format(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: {}".format(binascii.hexlify(key_line))) - - # These should all be "bytes" (Py3) or "str" (Py2) - # print(type(vendor)) - # print(type(signature)) - # print(type(user)) - - entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user) - - if verbose_logging: - print("Entropy: {}".format(binascii.hexlify(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: {}".format(binascii.hexlify(keykey))) - return keykey - - else: - print("Error number: {}".format(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): +def GetMasterKey(wineprefix): import subprocess, os, re verbose_logging = False @@ -319,7 +19,7 @@ def CryptUnprotectDataExecuteWine(wineprefix, data, entropy): print("Asking WINE to decrypt encrypted key for us ...") if wineprefix == "" or not os.path.exists(wineprefix): - print("Wineprefix not found!!") + print("Wineprefix not found!") return None @@ -334,27 +34,21 @@ def CryptUnprotectDataExecuteWine(wineprefix, data, entropy): if not line: break - stuff = re.match(r'#arch=(win32|win64)', line) - if (stuff): - winearch = stuff.groups()[0] + archkey = re.match(r'#arch=(win32|win64)', line) + if (archkey): + winearch = archkey.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).decode("utf-8") - env_dict["X_DECRYPT_ENTROPY"] = binascii.hexlify(entropy).decode("utf-8") - try: from calibre.utils.config import config_dir pluginsdir = os.path.join(config_dir,"plugins") @@ -366,37 +60,26 @@ def CryptUnprotectDataExecuteWine(wineprefix, data, entropy): # calls decrypt_win32.exe or decrypt_win64.exe proc = subprocess.Popen(["wine", "decrypt_" + winearch + ".exe"], shell=False, cwd=moddir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - prog_output, prog_stderr = proc.communicate() - stdout = prog_output.decode("utf-8") - stderr = prog_stderr.decode("utf-8") + prog_stdout, prog_stderr = proc.communicate() - if stdout.startswith("PROGOUTPUT:0:"): - key_string = stdout.split(':')[2] + if verbose_logging: + print("Stderr log:\n{}".format(prog_stderr.decode("utf-8"))) + print("Stdout log: {}".format(prog_stdout.decode("utf-8"))) + print("Exit code: {}".format(proc.returncode)) + + if proc.returncode == 0: if verbose_logging: - print("Successfully got encryption key from WINE: {}".format(key_string)) + print("Successfully got encryption key from WINE: {}".format(prog_stdout.decode("utf-8"))) else: print("Successfully got encryption key from WINE.") - master_key = binascii.unhexlify(key_string) - return True, master_key - - + master_key = binascii.unhexlify(prog_stdout) + return master_key else: - print("Huh. That didn't work. ") - try: - err = int(stdout.split(':')[1]) - if err == -4: - err = int(stdout.split(':')[2]) - new_serial = int(stdout.split(':')[3]) - if verbose_logging: - print("New serial: {}".format(new_serial)) - except: - err = None + print("Failed to extract encryption key from WINE.") + print("Exit code: {}".format(proc.returncode)) - if verbose_logging: - # print("Stderr log:\n{}".format(stderr)) - print("Program output: {}".format(stdout)) - return False, err + return None if __name__ == "__main__": diff --git a/calibre-plugin/keyextract/Makefile b/calibre-plugin/keyextract/Makefile index f25d6b8..f982222 100644 --- a/calibre-plugin/keyextract/Makefile +++ b/calibre-plugin/keyextract/Makefile @@ -5,10 +5,10 @@ clean: rm decrypt_win32.exe decrypt_win64.exe 2>/dev/null || /bin/true decrypt_win32.exe: main.c Makefile - i686-w64-mingw32-gcc main.c -Os -o decrypt_win32.exe -lcrypt32 + i686-w64-mingw32-gcc main.c -Os -o decrypt_win32.exe -lcrypt32 -lwsock32 i686-w64-mingw32-strip decrypt_win32.exe decrypt_win64.exe: main.c Makefile - x86_64-w64-mingw32-gcc main.c -Os -o decrypt_win64.exe -lcrypt32 + x86_64-w64-mingw32-gcc main.c -Os -o decrypt_win64.exe -lcrypt32 -lwsock32 x86_64-w64-mingw32-strip decrypt_win64.exe diff --git a/calibre-plugin/keyextract/main.c b/calibre-plugin/keyextract/main.c index 7870114..aa0e181 100644 --- a/calibre-plugin/keyextract/main.c +++ b/calibre-plugin/keyextract/main.c @@ -1,196 +1,112 @@ #include -#include -#include -#include -#include -#include -#include -#include +#include +#include -#ifdef DEBUG -#undef DEBUG -#endif +union CPUIDVendor { + unsigned int reg[3]; + char vendor[13]; +}; -int char2int(char input) { - if (input >= '0' && input <= '9') - return input - '0'; - if (input >= 'A' && input <= 'F') - return input - 'A' + 10; - if (input >= 'a' && input <= 'f') - return input - 'a' + 10; - - fputs("PROGOUTPUT:-3", stdout); - exit(-3); -} - -void hex2bin(const char * src, char * dst) { - while (*src && src[1]) { - *(dst++) = char2int(*src) * 16 + char2int(src[1]); - src += 2; - } -} - -#ifdef DEBUG -void hexDump ( - const char * desc, - const void * addr, - const int len, - int perLine -) { - // Silently ignore silly per-line values. - - if (perLine < 4 || perLine > 64) perLine = 16; - - int i; - unsigned char buff[perLine+1]; - const unsigned char * pc = (const unsigned char *)addr; - - // Output description if given. - - if (desc != NULL) fprintf (stderr, "%s:\n", desc); - - // Length checks. - - if (len == 0) { - fprintf(stderr, " ZERO LENGTH\n"); - return; - } - if (len < 0) { - fprintf(stderr, " NEGATIVE LENGTH: %d\n", len); - return; - } - - // Process every byte in the data. - - for (i = 0; i < len; i++) { - // Multiple of perLine means new or first line (with line offset). - - if ((i % perLine) == 0) { - // Only print previous-line ASCII buffer for lines beyond first. - - if (i != 0) fprintf (stderr, " %s\n", buff); - - // Output the offset of current line. - - fprintf (stderr, " %04x ", i); - } - - // Now the hex code for the specific character. - - fprintf (stderr, " %02x", pc[i]); - - // And buffer a printable ASCII character for later. - - if ((pc[i] < 0x20) || (pc[i] > 0x7e)) // isprint() may be better. - buff[i % perLine] = '.'; - else - buff[i % perLine] = pc[i]; - buff[(i % perLine) + 1] = '\0'; - } - - // Pad out last line if not exactly perLine characters. - - while ((i % perLine) != 0) { - fprintf (stderr, " "); - i++; - } - - // And print the final ASCII buffer. - - fprintf (stderr, " %s\n", buff); -} -#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; -} +struct EncEntropy { + unsigned int serial; + char vendor[12]; + char signature[3]; + char user[13]; +}; int main() { - char * var_data = "X_DECRYPT_DATA"; - char * var_entropy = "X_DECRYPT_ENTROPY"; - - char * data_hex = getenv(var_data); - char * entropy_hex = getenv(var_entropy); - - if (data_hex == NULL || entropy_hex == NULL) { - fputs("PROGOUTPUT:-1", stdout); - exit(-1); + // Get disk serial + DWORD serial; + if (GetVolumeInformation("c:\\\\", NULL, 0, &serial, NULL, NULL, NULL, 0) == 0) { + DWORD err = GetLastError(); + fprintf(stderr, "Error with GetVolumeInformation: %ld\n", err); + return err; } + DWORD be_serial = htonl(serial); + fprintf(stderr, "Disk serial (hex): %08lx\n", serial); - char * data_bytes = malloc((strlen(data_hex) / 2)); - char * entropy_bytes = malloc((strlen(entropy_hex) / 2)); - if (data_bytes == NULL || entropy_bytes == NULL) { - fputs("PROGOUTPUT:-2", stdout); - exit(-2); + unsigned int eax, ebx, ecx, edx; + // Get CPUID vendor string + union CPUIDVendor cpu_vendor; + if (__get_cpuid(0, &eax, &cpu_vendor.reg[0], &cpu_vendor.reg[2], &cpu_vendor.reg[1]) == 0) { + fprintf(stderr, "Error: cpuid(0) not supported"); + return 1; } - - hex2bin(data_hex, data_bytes); - hex2bin(entropy_hex, entropy_bytes); - -#ifdef DEBUG - hexDump("data", data_bytes, strlen(data_hex)/2, 16); - hexDump("entropy", entropy_bytes, strlen(entropy_hex)/2, 16); -#endif - - - -DATA_BLOB input_data; -DATA_BLOB entropy_data; - - -DATA_BLOB output_data; - -input_data.pbData = data_bytes; -input_data.cbData = strlen(data_hex)/2; - -entropy_data.pbData = entropy_bytes; -entropy_data.cbData = strlen(entropy_hex)/2; - - -int ret = CryptUnprotectData( - &input_data, - NULL, - &entropy_data, - NULL, - NULL, - 0, - &output_data); - - - -if (ret) { - if (output_data.cbData != 16) { - printf("PROGOUTPUT:-5:%d", output_data.cbData); - exit(-5); + cpu_vendor.vendor[12] = '\0'; + fprintf(stderr, "CPUID Vendor: %s\n", cpu_vendor.vendor); + // Get CPUID "signature" (eax of CPUID(1)) + unsigned int signature; + if (__get_cpuid(1, &signature, &ebx, &ecx, &edx) == 0) { + fprintf(stderr, "Error: cpuid(1) not supported"); + return 1; } - // Success! Return decrypted data - fputs("PROGOUTPUT:0:", stdout); + unsigned int be_signature = htonl(signature); + fprintf(stderr, "CPUID Signature (hex): %08x\n", signature); + + + // Get windows user + #define USERBUFSIZE 512 + TCHAR user[USERBUFSIZE]; + memset(&user, 0, sizeof(user)); // GetUserName only sets bytes as needed for length of username, but we need null bytes to fill the rest + DWORD bufsize = USERBUFSIZE ; + if (GetUserName(user, &bufsize) == 0) { + DWORD err = GetLastError(); + fprintf(stderr, "Error with GetUserName: %ld\n", err); + return err; + } + fprintf(stderr, "Username: %s\n", user); + + + // Get Encrypted adobe key + #define KEYBUFSIZE 180 // As measured + BYTE key[KEYBUFSIZE]; + DWORD regkeysize = KEYBUFSIZE; + LSTATUS retval = RegGetValue(HKEY_CURRENT_USER, "Software\\Adobe\\Adept\\Device", "key", RRF_RT_REG_BINARY, NULL, &key, ®keysize); + if (retval != ERROR_SUCCESS) { + fprintf(stderr, "Error with RegGetValue: %ld\n", retval); + fprintf(stderr, "regkeysize: %ld\n", regkeysize); + return retval; + } + fprintf(stderr, "Encrypted key (hex): "); + for (size_t i = 0; i < KEYBUFSIZE; i++ ) + { + fprintf(stderr, "%02x", key[i]); + } + fprintf(stderr, "\n"); + + + // Assemble "entropy" (passphrase) for key + struct EncEntropy entropy; + memcpy(&entropy.serial, &be_serial, sizeof(entropy.serial)); + memcpy(&entropy.vendor, &cpu_vendor.vendor, sizeof(entropy.vendor)); + memcpy(&entropy.signature, ((char*)(&be_signature))+1, sizeof(entropy.signature)); // Only the last 3 bytes are needed, hence the +1 ptr + memcpy(&entropy.user, &user, sizeof(entropy.user)); + + // Print entropy byte by byte in hex + fprintf(stderr, "Entropy: "); + for (size_t i = 0; i < sizeof(entropy); i++ ) + { + fprintf(stderr, "%02x", ((unsigned char*)&entropy)[i]); + } + fprintf(stderr, "\n"); + + + // Run decryption API + DATA_BLOB ciphertext_data, entropy_data, plaintext_data; + ciphertext_data.pbData = key; + ciphertext_data.cbData = sizeof(key); + entropy_data.pbData = (BYTE*)(&entropy); + entropy_data.cbData = sizeof(entropy); + if (CryptUnprotectData(&ciphertext_data, NULL, &entropy_data, NULL, NULL, 0, &plaintext_data) != TRUE) { + DWORD err = GetLastError(); + fprintf(stderr, "Error with CryptUnprotectData: %ld\n", err); + return err; + } + fprintf(stderr, "Decrypted key length: %lu\n", plaintext_data.cbData); + + // Print decrypted key to stdout for (int i = 0; i < 16; i++) { - printf("%02x", output_data.pbData[i]); + printf("%02x", plaintext_data.pbData[i]); } - exit(0); -} -else { - - // 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); -} - - -} - +} \ No newline at end of file