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.
|
||||
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):
|
||||
|
@ -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
|
||||
|
||||
|
||||
# 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 @@ class DeACSM(FileTypePlugin):
|
||||
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"]
|
||||
# Re-Extract modules
|
||||
|
||||
# mark that this version has been initialized
|
||||
os.mkdir(self.verdir)
|
||||
self.moddir = os.path.join(self.maindir,"modules")
|
||||
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():
|
||||
file_path = os.path.join(self.verdir, entry)
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except:
|
||||
pass
|
||||
names = ["cryptography.zip", "rsa.zip"]
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -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 @@ Calibre on all operating systems without additional dependencies.
|
||||
|
||||
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 "<?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"))
|
||||
|
||||
m10m = datetime.utcnow() + timedelta(minutes=10)
|
||||
m10m = dt + timedelta(minutes=10)
|
||||
m10m_str = m10m.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
ret += "<adept:expiration>%s</adept:expiration>" % (m10m_str)
|
||||
@ -567,71 +541,46 @@ def activateDevice():
|
||||
|
||||
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:
|
||||
|
||||
f = open(FILE_ACTIVATIONXML, "r")
|
||||
old_xml = f.read().replace("</activationInfo>", "")
|
||||
f.close()
|
||||
|
||||
f = open(FILE_ACTIVATIONXML, "w")
|
||||
|
||||
|
||||
'''
|
||||
|
||||
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.write(old_xml)
|
||||
f.write(ret.decode("latin-1"))
|
||||
f.write("</activationInfo>\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)
|
||||
|
||||
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:
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user