From 3962c91d2267c1cbbb47b19b17b3eac20c01dacb Mon Sep 17 00:00:00 2001 From: Florian Bach Date: Fri, 24 Sep 2021 16:10:03 +0200 Subject: [PATCH] Python account activation finally working --- README.md | 14 +- calibre-plugin/__init__.py | 97 ++++++++---- .../register_ADE_account.py | 140 +++++------------- 3 files changed, 112 insertions(+), 139 deletions(-) rename register_ADE_account.py => calibre-plugin/register_ADE_account.py (84%) diff --git a/README.md b/README.md index 3d6fbfc..b6a715a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,20 @@ -# Calibe ACSM plugin (Linux only) +# Calibre ACSM plugin (Linux only) This is a Linux-only Calibre plugin that allows you to turn ACSM files into EPUBs without the need for ADE. It's based on libgourou by Grégory Soutadé (http://indefero.soutade.fr/p/libgourou/). +## Note + +The source code in this repository currently doesn't work because it's being updated. If you want to check the currently working source, check out the Release, the plugin source is included in the ZIP, and there's also an additional tar.xz with the libgourou source. + +## Plans for the future + +Right now this plugin is for Linux only, as it's using libgourou by Grégory Soutadé which only supports Linux. + +I am currently reimplementing this library in Python so it will work on all operating systems in the future. Current state of that implementation is that account authorization works, so all that's now missing is the actual Fulfillment. And potentially allowing anonymous accounts in the future. + +This Python implementation isn't useable to normal end-users right now, so if you're on Linux please use the released plugin instead of the raw source code, and if you're on Windows or on MacOS please wait for the native Python version of this plugin. + ## Setup You need to have the following packages installed to use this plugin (Debian Bullseye / Ubuntu 20.04): diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index 6516256..49bcc35 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -25,12 +25,27 @@ from calibre.gui2 import (question_dialog, error_dialog, info_dialog, choose_save_file) # type: ignore +# Test - need these: +import sys, os, zipfile, shutil, pwd, hashlib, base64, locale, urllib.request, datetime +from datetime import datetime, timedelta + + +from Crypto import Random +from Crypto.PublicKey import RSA +from Crypto.Util.asn1 import DerSequence +from Crypto.Hash import SHA +from Crypto.Cipher import AES +from Crypto.Cipher import PKCS1_v1_5 +from uuid import getnode +from lxml import etree + + class DeACSM(FileTypePlugin): name = PLUGIN_NAME - description = "Takes an Adobe ACSM file and converts that into a useable EPUB file." + description = "Takes an Adobe ACSM file and converts that into a useable EPUB file. Python reimplementation of libgourou by Grégory Soutadé" supported_platforms = ['linux'] - author = "Leseratte10 (Plugin), Grégory Soutadé (libgourou)" + author = "Leseratte10" version = PLUGIN_VERSION_TUPLE minimum_calibre_version = (5, 0, 0) file_types = set(['acsm']) @@ -40,8 +55,10 @@ class DeACSM(FileTypePlugin): def initialize(self): """ - On initialization, make sure the libgourou code is present for compilation. + On initialization, make sure we have all the libraries (python-rsa and cryptography) + that we need. """ + try: self.pluginsdir = os.path.join(config_dir,"plugins") if not os.path.exists(self.pluginsdir): @@ -50,39 +67,57 @@ def initialize(self): if not os.path.exists(self.maindir): os.mkdir(self.maindir) - # only continue if we've never run this version of the plugin before - self.verdir = os.path.join(self.maindir,PLUGIN_VERSION) - if not os.path.exists(self.verdir): - if iswindows or isosx: - print("Windows and MacOS not supported!") - return - else: - names = ["libgourou_bundle_release.tar.xz"] - - # mark that this version has been initialized - os.mkdir(self.verdir) - - lib_dict = self.load_resources(names) - print("{0} v{1}: Copying needed library files from plugin zip".format(PLUGIN_NAME, PLUGIN_VERSION)) - - for entry, data in lib_dict.items(): - file_path = os.path.join(self.verdir, entry) - try: - os.remove(file_path) - except: - pass - - try: - open(file_path,'wb').write(data) - except: - print("{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION)) - traceback.print_exc() - pass + # Re-Extract modules + + self.moddir = os.path.join(self.maindir,"modules") + if os.path.exists(self.moddir): + shutil.rmtree(self.moddir, ignore_errors=True) + + os.mkdir(self.moddir) + + names = ["cryptography.zip", "rsa.zip"] + + lib_dict = self.load_resources(names) + print("{0} v{1}: Copying needed library files from plugin zip".format(PLUGIN_NAME, PLUGIN_VERSION)) + + for entry, data in lib_dict.items(): + file_path = os.path.join(self.moddir, entry) + try: + os.remove(file_path) + except: + pass + + try: + open(file_path,'wb').write(data) + with zipfile.ZipFile(file_path, 'r') as ref: + ref.extractall(self.moddir) + os.remove(file_path) + + except: + print("{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION)) + traceback.print_exc() + pass + + try: + from cryptography.hazmat.primitives.serialization import pkcs12 as pkcs12module + except: + sys.path.insert(0, os.path.join(self.moddir, "cryptography")) + from cryptography.hazmat.primitives.serialization import pkcs12 as pkcs12module + + try: + import rsa + except: + sys.path.insert(0, os.path.join(self.moddir, "rsa")) + import rsa except Exception as e: traceback.print_exc() raise + + + + def is_customizable(self): return True diff --git a/register_ADE_account.py b/calibre-plugin/register_ADE_account.py similarity index 84% rename from register_ADE_account.py rename to calibre-plugin/register_ADE_account.py index 30b2b2c..8d40df9 100644 --- a/register_ADE_account.py +++ b/calibre-plugin/register_ADE_account.py @@ -2,9 +2,8 @@ # -*- coding: utf-8 -*- ''' -This is an experimental Python version of libgourou. Right now it only supports part of the authorization -(and doesn't support fulfillment at all). All the encryption / decryption stuff works, but once I send -the final request to the Adobe server, it responds with E_AUTH_USER_AUTH, and I have no idea what that means. +This is an experimental Python version of libgourou. Right now it only supports authorization, +it does not yet support ACSM fulfillment. Who knows, maybe there will someday be a full Python version of libgourou so it can be used in Calibre on all operating systems without additional dependencies. @@ -15,16 +14,17 @@ import os, pwd, hashlib, base64, locale, urllib.request, datetime from datetime import datetime, timedelta -from OpenSSL import crypto from Crypto import Random from Crypto.PublicKey import RSA from Crypto.Util.asn1 import DerSequence -from Crypto.Signature import PKCS1_v1_5 as pkcssign from Crypto.Hash import SHA from Crypto.Cipher import AES from Crypto.Cipher import PKCS1_v1_5 from uuid import getnode +from cryptography.hazmat.primitives import serialization from lxml import etree +from cryptography.hazmat.primitives.serialization import pkcs12 as pkcs12module +import rsa VAR_MAIL = "test@example.com" @@ -298,27 +298,6 @@ def buildSignInRequest(adobeID: str, adobePassword: str, authenticationCertifica global authkey_pub, authkey_priv, licensekey_pub, licensekey_priv - # original - #// Generate Auth key and License Key - #void* rsaAuth = client->generateRSAKey(1024); - #void* rsaLicense = client->generateRSAKey(1024); - - #std::string serializedData = serializeRSAPublicKey(rsaAuth); - #appendTextElem(signIn, "adept:publicAuthKey", serializedData); - #serializedData = serializeRSAPrivateKey(rsaAuth); - #appendTextElem(signIn, "adept:encryptedPrivateAuthKey", serializedData.data()); - # - - #void* DRMProcessorClientImpl::generateRSAKey(int keyLengthBits) - #{ - #BIGNUM * bn = BN_new(); - #RSA * rsa = RSA_new(); - #BN_set_word(bn, 0x10001); - #RSA_generate_key_ex(rsa, keyLengthBits, bn, 0); - #BN_free(bn); - #return rsa; - #} - authkey_pub = authkey.publickey().exportKey("DER") authkey_priv = authkey.exportKey("DER", pkcs=8) authkey_priv_enc = encrypt_with_device_key(authkey_priv) @@ -327,9 +306,6 @@ def buildSignInRequest(adobeID: str, adobePassword: str, authenticationCertifica licensekey_priv = licensekey.exportKey("DER", pkcs=8) licensekey_priv_enc = encrypt_with_device_key(licensekey_priv) - print("authkey_priv is") - print(base64.b64encode(authkey_priv)) - etree.SubElement(root, etree.QName(NSMAP["adept"], "publicAuthKey")).text = base64.b64encode(authkey_pub) etree.SubElement(root, etree.QName(NSMAP["adept"], "encryptedPrivateAuthKey")).text = base64.b64encode(authkey_priv_enc) @@ -337,8 +313,6 @@ def buildSignInRequest(adobeID: str, adobePassword: str, authenticationCertifica etree.SubElement(root, etree.QName(NSMAP["adept"], "publicLicenseKey")).text = base64.b64encode(licensekey_pub) etree.SubElement(root, etree.QName(NSMAP["adept"], "encryptedPrivateLicenseKey")).text = base64.b64encode(licensekey_priv_enc) - # print(etree.tostring(root, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1")) - return "\n" + etree.tostring(root, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1") @@ -477,7 +451,7 @@ def addNonce(): ret += "%s" % (base64.b64encode(final).decode("latin-1")) - m10m = datetime.utcnow() + timedelta(minutes=10) + m10m = dt + timedelta(minutes=10) m10m_str = m10m.strftime("%Y-%m-%dT%H:%M:%SZ") ret += "%s" % (m10m_str) @@ -566,72 +540,47 @@ def activateDevice(): data = "\n" + etree.tostring(req_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1") ret = sendRequestDocu(data, VAR_ACS_SERVER + "/Activate") - - print("======================================================") - print("Sending request to " + VAR_ACS_SERVER + "/Activate") - print("Payload:") - print(data) - print("got response:") + + print("Response from server: ") print(ret) - print("======================================================") - - - - -''' + # Soooo, lets go and append that to the XML: - void DRMProcessor::activateDevice() - { - pugi::xml_document activateReq; - - GOUROU_LOG(INFO, "Activate device"); - - buildActivateReq(activateReq); - - pugi::xml_node root = activateReq.select_node("adept:activate").node(); - - std::string signature = signNode(root); - - root = activateReq.select_node("adept:activate").node(); - appendTextElem(root, "adept:signature", signature); - - pugi::xml_document activationDoc; - user->readActivation(activationDoc); - - std::string activationURL = user->getProperty("//adept:activationURL"); - activationURL += "/Activate"; - - ByteArray reply = sendRequest(activateReq, activationURL); - - pugi::xml_document activationToken; - activationToken.load_buffer(reply.data(), reply.length()); - - root = activationDoc.select_node("activationInfo").node(); - root.append_copy(activationToken.first_child()); - user->updateActivationFile(activationDoc); - } - -''' + f = open(FILE_ACTIVATIONXML, "r") + old_xml = f.read().replace("", "") + f.close() + + f = open(FILE_ACTIVATIONXML, "w") + f.write(old_xml) + f.write(ret.decode("latin-1")) + f.write("\n") + f.close() + def sign_node(node): sha_hash = hash_node(node) + sha_hash = sha_hash.digest() global devkey_bytes global pkcs12 my_pkcs12 = base64.b64decode(pkcs12) - pkcs_data = crypto.load_pkcs12(my_pkcs12, base64.b64encode(devkey_bytes)) - my_priv_key = crypto.dump_privatekey(crypto.FILETYPE_ASN1, pkcs_data.get_privatekey()) + my_priv_key, _, _ = pkcs12module.load_key_and_certificates(my_pkcs12, base64.b64encode(devkey_bytes)) + my_priv_key = my_priv_key.private_bytes(serialization.Encoding.DER, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()) + - key = RSA.importKey(my_priv_key) - cipherAC = pkcssign.new(key) - crypted_msg = cipherAC.sign(sha_hash) + key = rsa.PrivateKey.load_pkcs1(RSA.importKey(my_priv_key).exportKey()) + keylen = rsa.pkcs1.common.byte_size(key.n) + padded = rsa.pkcs1._pad_for_signing(sha_hash, keylen) + payload = rsa.pkcs1.transform.bytes2int(padded) + encrypted = key.blinded_encrypt(payload) + block = rsa.pkcs1.transform.int2bytes(encrypted, keylen) + signature = base64.b64encode(block).decode() - return base64.b64encode(crypted_msg) + return signature @@ -666,8 +615,8 @@ def hash_node_ctx(node, hash_ctx): for attribute in attrKeys: hash_do_append_tag(hash_ctx, ASN_ATTRIBUTE) hash_do_append_string(hash_ctx, "") - hash_do_append_string(hash_ctx, attribute) # "requestType" - hash_do_append_string(hash_ctx, node.get(attribute)) # "initial" + hash_do_append_string(hash_ctx, attribute) + hash_do_append_string(hash_ctx, node.get(attribute)) if (node.text is not None): @@ -687,34 +636,11 @@ def hash_do_append_string(hash_ctx, string: str): len_upper = int(length / 256) len_lower = int(length & 0xFF) - global debug - - if debug: - print("[STR %02x %02x => %s ]" % (len_upper, len_lower, string)) - hash_do_append_raw_bytes(hash_ctx, [len_upper, len_lower]) hash_do_append_raw_bytes(hash_ctx, bytes(string, encoding="latin-1")) def hash_do_append_tag(hash_ctx, tag: int): - global debug - - if debug: - if (tag == ASN_NONE): - print("[TAG ASN_NONE (0) ]") - elif (tag == ASN_NS_TAG): - print("[TAG ASN_NS_TAG (1) ]") - elif (tag == ASN_CHILD): - print("[TAG ASN_CHILD (2) ]") - elif (tag == ASN_END_TAG): - print("[TAG ASN_END_TAG (3) ]") - elif (tag == ASN_TEXT): - print("[TAG ASN_TEXT (4) ]") - elif (tag == ASN_ATTRIBUTE): - print("[TAG ASN_ATTRIBUTE (5) ]") - else: - print("[ INVALID TAG!!!! %d" % (tag)) - if (tag > 5): return