mirror of
https://github.com/Leseratte10/acsm-calibre-plugin.git
synced 2024-12-22 17:29:56 +06:00
Python account activation finally working
This commit is contained in:
parent
391d7082e9
commit
3962c91d22
14
README.md
14
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.
|
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/).
|
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
|
## Setup
|
||||||
|
|
||||||
You need to have the following packages installed to use this plugin (Debian Bullseye / Ubuntu 20.04):
|
You need to have the following packages installed to use this plugin (Debian Bullseye / Ubuntu 20.04):
|
||||||
|
@ -25,12 +25,27 @@ from calibre.constants import iswindows, isosx # type: ignore
|
|||||||
from calibre.gui2 import (question_dialog, error_dialog, info_dialog, choose_save_file) # type: ignore
|
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):
|
class DeACSM(FileTypePlugin):
|
||||||
name = PLUGIN_NAME
|
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']
|
supported_platforms = ['linux']
|
||||||
author = "Leseratte10 (Plugin), Grégory Soutadé (libgourou)"
|
author = "Leseratte10"
|
||||||
version = PLUGIN_VERSION_TUPLE
|
version = PLUGIN_VERSION_TUPLE
|
||||||
minimum_calibre_version = (5, 0, 0)
|
minimum_calibre_version = (5, 0, 0)
|
||||||
file_types = set(['acsm'])
|
file_types = set(['acsm'])
|
||||||
@ -40,8 +55,10 @@ class DeACSM(FileTypePlugin):
|
|||||||
|
|
||||||
def initialize(self):
|
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:
|
try:
|
||||||
self.pluginsdir = os.path.join(config_dir,"plugins")
|
self.pluginsdir = os.path.join(config_dir,"plugins")
|
||||||
if not os.path.exists(self.pluginsdir):
|
if not os.path.exists(self.pluginsdir):
|
||||||
@ -50,39 +67,57 @@ class DeACSM(FileTypePlugin):
|
|||||||
if not os.path.exists(self.maindir):
|
if not os.path.exists(self.maindir):
|
||||||
os.mkdir(self.maindir)
|
os.mkdir(self.maindir)
|
||||||
|
|
||||||
# only continue if we've never run this version of the plugin before
|
# Re-Extract modules
|
||||||
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
|
self.moddir = os.path.join(self.maindir,"modules")
|
||||||
os.mkdir(self.verdir)
|
if os.path.exists(self.moddir):
|
||||||
|
shutil.rmtree(self.moddir, ignore_errors=True)
|
||||||
lib_dict = self.load_resources(names)
|
|
||||||
print("{0} v{1}: Copying needed library files from plugin zip".format(PLUGIN_NAME, PLUGIN_VERSION))
|
os.mkdir(self.moddir)
|
||||||
|
|
||||||
for entry, data in lib_dict.items():
|
names = ["cryptography.zip", "rsa.zip"]
|
||||||
file_path = os.path.join(self.verdir, entry)
|
|
||||||
try:
|
lib_dict = self.load_resources(names)
|
||||||
os.remove(file_path)
|
print("{0} v{1}: Copying needed library files from plugin zip".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
for entry, data in lib_dict.items():
|
||||||
open(file_path,'wb').write(data)
|
file_path = os.path.join(self.moddir, entry)
|
||||||
except:
|
try:
|
||||||
print("{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION))
|
os.remove(file_path)
|
||||||
traceback.print_exc()
|
except:
|
||||||
pass
|
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:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def is_customizable(self):
|
def is_customizable(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -2,9 +2,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
'''
|
'''
|
||||||
This is an experimental Python version of libgourou. Right now it only supports part of the authorization
|
This is an experimental Python version of libgourou. Right now it only supports authorization,
|
||||||
(and doesn't support fulfillment at all). All the encryption / decryption stuff works, but once I send
|
it does not yet support ACSM fulfillment.
|
||||||
the final request to the Adobe server, it responds with E_AUTH_USER_AUTH, and I have no idea what that means.
|
|
||||||
|
|
||||||
Who knows, maybe there will someday be a full Python version of libgourou so it can be used in
|
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.
|
Calibre on all operating systems without additional dependencies.
|
||||||
@ -15,16 +14,17 @@ Calibre on all operating systems without additional dependencies.
|
|||||||
|
|
||||||
import os, pwd, hashlib, base64, locale, urllib.request, datetime
|
import os, pwd, hashlib, base64, locale, urllib.request, datetime
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from OpenSSL import crypto
|
|
||||||
from Crypto import Random
|
from Crypto import Random
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from Crypto.Util.asn1 import DerSequence
|
from Crypto.Util.asn1 import DerSequence
|
||||||
from Crypto.Signature import PKCS1_v1_5 as pkcssign
|
|
||||||
from Crypto.Hash import SHA
|
from Crypto.Hash import SHA
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
from Crypto.Cipher import PKCS1_v1_5
|
from Crypto.Cipher import PKCS1_v1_5
|
||||||
from uuid import getnode
|
from uuid import getnode
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from cryptography.hazmat.primitives.serialization import pkcs12 as pkcs12module
|
||||||
|
import rsa
|
||||||
|
|
||||||
|
|
||||||
VAR_MAIL = "test@example.com"
|
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
|
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_pub = authkey.publickey().exportKey("DER")
|
||||||
authkey_priv = authkey.exportKey("DER", pkcs=8)
|
authkey_priv = authkey.exportKey("DER", pkcs=8)
|
||||||
authkey_priv_enc = encrypt_with_device_key(authkey_priv)
|
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 = licensekey.exportKey("DER", pkcs=8)
|
||||||
licensekey_priv_enc = encrypt_with_device_key(licensekey_priv)
|
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"], "publicAuthKey")).text = base64.b64encode(authkey_pub)
|
||||||
etree.SubElement(root, etree.QName(NSMAP["adept"], "encryptedPrivateAuthKey")).text = base64.b64encode(authkey_priv_enc)
|
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"], "publicLicenseKey")).text = base64.b64encode(licensekey_pub)
|
||||||
etree.SubElement(root, etree.QName(NSMAP["adept"], "encryptedPrivateLicenseKey")).text = base64.b64encode(licensekey_priv_enc)
|
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 "<?xml version=\"1.0\"?>\n" + etree.tostring(root, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1")
|
return "<?xml version=\"1.0\"?>\n" + etree.tostring(root, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1")
|
||||||
|
|
||||||
|
|
||||||
@ -477,7 +451,7 @@ def addNonce():
|
|||||||
|
|
||||||
ret += "<adept:nonce>%s</adept:nonce>" % (base64.b64encode(final).decode("latin-1"))
|
ret += "<adept:nonce>%s</adept:nonce>" % (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")
|
m10m_str = m10m.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
ret += "<adept:expiration>%s</adept:expiration>" % (m10m_str)
|
ret += "<adept:expiration>%s</adept:expiration>" % (m10m_str)
|
||||||
@ -566,72 +540,47 @@ def activateDevice():
|
|||||||
data = "<?xml version=\"1.0\"?>\n" + 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")
|
||||||
|
|
||||||
ret = sendRequestDocu(data, VAR_ACS_SERVER + "/Activate")
|
ret = sendRequestDocu(data, VAR_ACS_SERVER + "/Activate")
|
||||||
|
|
||||||
print("======================================================")
|
print("Response from server: ")
|
||||||
print("Sending request to " + VAR_ACS_SERVER + "/Activate")
|
|
||||||
print("Payload:")
|
|
||||||
print(data)
|
|
||||||
print("got response:")
|
|
||||||
print(ret)
|
print(ret)
|
||||||
print("======================================================")
|
|
||||||
|
|
||||||
|
# Soooo, lets go and append that to the XML:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
void DRMProcessor::activateDevice()
|
f = open(FILE_ACTIVATIONXML, "r")
|
||||||
{
|
old_xml = f.read().replace("</activationInfo>", "")
|
||||||
pugi::xml_document activateReq;
|
f.close()
|
||||||
|
|
||||||
GOUROU_LOG(INFO, "Activate device");
|
f = open(FILE_ACTIVATIONXML, "w")
|
||||||
|
|
||||||
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.write(old_xml)
|
||||||
|
f.write(ret.decode("latin-1"))
|
||||||
|
f.write("</activationInfo>\n")
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
def sign_node(node):
|
def sign_node(node):
|
||||||
|
|
||||||
sha_hash = hash_node(node)
|
sha_hash = hash_node(node)
|
||||||
|
sha_hash = sha_hash.digest()
|
||||||
|
|
||||||
global devkey_bytes
|
global devkey_bytes
|
||||||
global pkcs12
|
global pkcs12
|
||||||
|
|
||||||
my_pkcs12 = base64.b64decode(pkcs12)
|
my_pkcs12 = base64.b64decode(pkcs12)
|
||||||
|
|
||||||
pkcs_data = crypto.load_pkcs12(my_pkcs12, base64.b64encode(devkey_bytes))
|
my_priv_key, _, _ = pkcs12module.load_key_and_certificates(my_pkcs12, base64.b64encode(devkey_bytes))
|
||||||
my_priv_key = crypto.dump_privatekey(crypto.FILETYPE_ASN1, pkcs_data.get_privatekey())
|
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)
|
|
||||||
|
|
||||||
return base64.b64encode(crypted_msg)
|
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 signature
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -666,8 +615,8 @@ def hash_node_ctx(node, hash_ctx):
|
|||||||
for attribute in attrKeys:
|
for attribute in attrKeys:
|
||||||
hash_do_append_tag(hash_ctx, ASN_ATTRIBUTE)
|
hash_do_append_tag(hash_ctx, ASN_ATTRIBUTE)
|
||||||
hash_do_append_string(hash_ctx, "")
|
hash_do_append_string(hash_ctx, "")
|
||||||
hash_do_append_string(hash_ctx, attribute) # "requestType"
|
hash_do_append_string(hash_ctx, attribute)
|
||||||
hash_do_append_string(hash_ctx, node.get(attribute)) # "initial"
|
hash_do_append_string(hash_ctx, node.get(attribute))
|
||||||
|
|
||||||
|
|
||||||
if (node.text is not None):
|
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_upper = int(length / 256)
|
||||||
len_lower = int(length & 0xFF)
|
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, [len_upper, len_lower])
|
||||||
hash_do_append_raw_bytes(hash_ctx, bytes(string, encoding="latin-1"))
|
hash_do_append_raw_bytes(hash_ctx, bytes(string, encoding="latin-1"))
|
||||||
|
|
||||||
def hash_do_append_tag(hash_ctx, tag: int):
|
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):
|
if (tag > 5):
|
||||||
return
|
return
|
||||||
|
|
Loading…
Reference in New Issue
Block a user