diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 0cd3b29..b186cca 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -32,7 +32,8 @@ jobs:
- name: Install dependencies
run: |
- pip3 install freezegun lxml pycryptodome rsa oscrypto
+ # Require cryptography >= 3.1 because in 3.0 and below, the backend param in load_key_and_certificates was still required.
+ pip3 install freezegun lxml pycryptodome rsa oscrypto "cryptography>=3.1"
- name: Run tests
run: |
diff --git a/bundle_calibre_plugin.sh b/bundle_calibre_plugin.sh
index fcc2b06..2cc90a7 100755
--- a/bundle_calibre_plugin.sh
+++ b/bundle_calibre_plugin.sh
@@ -11,6 +11,9 @@ make
popd
+# Delete cache
+rm -r __pycache__
+
# Set module ID. This needs to be changed if any of the module ZIPs change.
echo -n "2021-12-19-03" > module_id.txt
diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py
index e435d7e..00d3b27 100644
--- a/calibre-plugin/__init__.py
+++ b/calibre-plugin/__init__.py
@@ -35,7 +35,8 @@
# update python-cryptography from 3.4.8 to 36.0.1, update python-rsa from 4.7.2 to 4.8.
# Currently in development:
# Ignore fatal HTTP errors during optional fulfillment notifications,
-# allow authorizing an eReader through USB.
+# allow authorizing an eReader through USB, add a ton of testing code,
+# drop dependencies python-cryptography, python-rsa and python-pyasn1.
PLUGIN_NAME = "DeACSM"
PLUGIN_VERSION_TUPLE = (0, 0, 15)
diff --git a/calibre-plugin/customRSA.py b/calibre-plugin/customRSA.py
index e442fb9..416d1a5 100644
--- a/calibre-plugin/customRSA.py
+++ b/calibre-plugin/customRSA.py
@@ -8,8 +8,23 @@ The original code used blinding and this one doesn't,
but we don't really care about side-channel attacks ...
'''
+try:
+ from Cryptodome.PublicKey import RSA
+except ImportError:
+ # Some distros still ship this as Crypto
+ from Crypto.PublicKey import RSA
+
class CustomRSA:
+ def encrypt_for_adobe_signature(signing_key, message):
+ key = RSA.importKey(signing_key)
+ keylen = CustomRSA.byte_size(key.n)
+ padded = CustomRSA.pad_message(message, keylen)
+ payload = CustomRSA.transform_bytes2int(padded)
+ encrypted = CustomRSA.normal_encrypt(key, payload)
+ block = CustomRSA.transform_int2bytes(encrypted, keylen)
+ return block
+
def byte_size(number: int):
return (number.bit_length() + 7) // 8
diff --git a/calibre-plugin/libadobe.py b/calibre-plugin/libadobe.py
index a4420d1..ade078d 100644
--- a/calibre-plugin/libadobe.py
+++ b/calibre-plugin/libadobe.py
@@ -13,18 +13,16 @@ from datetime import datetime, timedelta
from lxml import etree
try:
- from Crypto import Random
- from Crypto.Cipher import AES
- from Crypto.PublicKey import RSA
- from Crypto.Hash import SHA
-except ImportError:
- # Debian (and Ubuntu) ship pycryptodome, but not in its compatible mode with pycrypto
- # If `Crypto` can't be found, try under pycryptodome's own namespace
from Cryptodome import Random
from Cryptodome.Cipher import AES
- from Cryptodome.PublicKey import RSA
from Cryptodome.Hash import SHA
+except ImportError:
+ # Some distros still ship Crypto
+ from Crypto import Random
+ from Crypto.Cipher import AES
+ from Crypto.Hash import SHA
+
try:
from customRSA import CustomRSA
except:
@@ -176,9 +174,11 @@ def makeFingerprint(serial: str):
# base64(sha1(serial + privateKey))
# Fingerprint must be 20 bytes or less.
- f = open(FILE_DEVICEKEY, "rb")
- devkey_bytes = f.read()
- f.close()
+ global devkey_bytes
+ if devkey_bytes is None:
+ f = open(FILE_DEVICEKEY, "rb")
+ devkey_bytes = f.read()
+ f.close()
str_to_hash = serial + devkey_bytes.decode('latin-1')
hashed_str = hashlib.sha1(str_to_hash.encode('latin-1')).digest()
@@ -416,6 +416,8 @@ def sign_node(node):
devkey_bytes = f.read()
f.close()
+ # Get private key
+
try:
activationxml = etree.parse(FILE_ACTIVATIONXML)
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
@@ -424,17 +426,12 @@ def sign_node(node):
return None
my_pkcs12 = base64.b64decode(pkcs12)
-
my_priv_key, _, _ = keys.parse_pkcs12(my_pkcs12, base64.b64encode(devkey_bytes))
my_priv_key = dump_private_key(my_priv_key, None, "der")
+ # textbook RSA with that private key
- key = RSA.importKey(my_priv_key)
- keylen = CustomRSA.byte_size(key.n)
- padded = CustomRSA.pad_message(sha_hash, keylen)
- payload = CustomRSA.transform_bytes2int(padded)
- encrypted = CustomRSA.normal_encrypt(key, payload)
- block = CustomRSA.transform_int2bytes(encrypted, keylen)
+ block = CustomRSA.encrypt_for_adobe_signature(my_priv_key, sha_hash)
signature = base64.b64encode(block).decode()
# Debug
diff --git a/calibre-plugin/libadobeAccount.py b/calibre-plugin/libadobeAccount.py
index e369828..ae201b4 100644
--- a/calibre-plugin/libadobeAccount.py
+++ b/calibre-plugin/libadobeAccount.py
@@ -3,15 +3,15 @@ import base64
import locale, platform
try:
- from Crypto.PublicKey import RSA
- from Crypto.Util.asn1 import DerSequence
- from Crypto.Cipher import PKCS1_v1_5
-except ImportError:
- # Debian (and Ubuntu) ship pycryptodome, but not in its compatible mode with pycrypto
- # If `Crypto` can't be found, try under pycryptodome's own namespace
from Cryptodome.PublicKey import RSA
from Cryptodome.Util.asn1 import DerSequence
from Cryptodome.Cipher import PKCS1_v1_5
+except ImportError:
+ # Some distros ship this as Crypto still.
+ from Crypto.PublicKey import RSA
+ from Crypto.Util.asn1 import DerSequence
+ from Crypto.Cipher import PKCS1_v1_5
+
try:
from libadobe import addNonce, sign_node, sendRequestDocu, sendHTTPRequest
diff --git a/calibre-plugin/libadobeImportAccount.py b/calibre-plugin/libadobeImportAccount.py
index f2a4bc0..ab1b62b 100644
--- a/calibre-plugin/libadobeImportAccount.py
+++ b/calibre-plugin/libadobeImportAccount.py
@@ -3,11 +3,10 @@ import base64
import os, locale, platform
try:
- from Crypto.Cipher import AES as _AES
-except ImportError:
- # Debian (and Ubuntu) ship pycryptodome, but not in its compatible mode with pycrypto
- # If `Crypto` can't be found, try under pycryptodome's own namespace
from Cryptodome.Cipher import AES as _AES
+except ImportError:
+ # Some distros still ship this as Crypto
+ from Crypto.Cipher import AES as _AES
class AES(object):
def __init__(self, key, iv):
diff --git a/calibre-plugin/register_ADE_account.py b/calibre-plugin/register_ADE_account.py
index 50ceeeb..47c73a4 100644
--- a/calibre-plugin/register_ADE_account.py
+++ b/calibre-plugin/register_ADE_account.py
@@ -66,7 +66,7 @@ def main():
print("Login unsuccessful: " + resp)
exit(1)
- success, resp = activateDevice(VAR_VER)
+ success, resp = activateDevice(VAR_VER, None)
if (success is False):
print("Couldn't activate device: " + resp)
exit(1)
diff --git a/tests/main.py b/tests/main.py
index 9e0d9af..7d844cb 100755
--- a/tests/main.py
+++ b/tests/main.py
@@ -1,6 +1,6 @@
-#!/usr/bin/python3 -W ignore::DeprecationWarning
+#!/usr/bin/python3
-import sys
+import sys, os
sys.path.append("../calibre-plugin")
@@ -38,23 +38,43 @@ class TestAdobe(unittest.TestCase):
def test_checkIfVersionListsAreValid(self):
- '''These lists must have the same amount of elements, otherwise stuff will break.'''
+ '''
+ Check if version lists are sane
+ These four lists must all have the same amount of elements.
+ Also, the default build ID must be valid, and all the IDs
+ available for authorization or switching must be valid, too.
+ '''
+
+ self.assertIn(libadobe.VAR_VER_DEFAULT_BUILD_ID, libadobe.VAR_VER_BUILD_IDS, "Default build ID invalid")
self.assertEqual(len(libadobe.VAR_VER_SUPP_CONFIG_NAMES), len(libadobe.VAR_VER_SUPP_VERSIONS), "Version lists seem to be invalid")
self.assertEqual(len(libadobe.VAR_VER_SUPP_CONFIG_NAMES), len(libadobe.VAR_VER_HOBBES_VERSIONS), "Version lists seem to be invalid")
self.assertEqual(len(libadobe.VAR_VER_SUPP_CONFIG_NAMES), len(libadobe.VAR_VER_OS_IDENTIFIERS), "Version lists seem to be invalid")
self.assertEqual(len(libadobe.VAR_VER_SUPP_CONFIG_NAMES), len(libadobe.VAR_VER_BUILD_IDS), "Version lists seem to be invalid")
+ for version in libadobe.VAR_VER_ALLOWED_BUILD_IDS_AUTHORIZE:
+ self.assertIn(version, libadobe.VAR_VER_BUILD_IDS, "Invalid buildID for authorization")
+ for version in libadobe.VAR_VER_ALLOWED_BUILD_IDS_SWITCH_TO:
+ self.assertIn(version, libadobe.VAR_VER_BUILD_IDS, "Invalid buildID for switching")
+
def test_checkSystemIdentifiers(self):
- '''Check if we can figure out necessary system IDs.'''
+ '''Check if we can figure out necessary system IDs'''
self.assertEqual((len(libadobe.get_mac_address())), 6, "MAC address invalid")
+ self.assertEqual(libadobe.get_mac_address(), libadobe.get_mac_address(), "MAC address not constant")
self.assertEqual((len(libadobe.makeSerial(False))), 40, "SHA1 hash for device serial invalid (device-based)")
self.assertEqual((len(libadobe.makeSerial(True))), 40, "SHA1 hash for device serial invalid (random)")
+ def test_fingerprintGeneration(self):
+ '''Check if fingerprint generation works'''
+ libadobe.devkey_bytes = bytes([0xf8, 0x7a, 0xfc, 0x8c, 0x75, 0x25, 0xdc, 0x4b, 0x83, 0xec, 0x0c, 0xe2, 0xab, 0x4b, 0xef, 0x51])
+
+ self.assertEqual(libadobe.makeFingerprint("f0081bce3f771bdeeb26fcb4b2011fed77edff7b"), b"FgLMNXxv1BZPqMOM6IUnfaG4Qj8=", "Wrong fingerprint")
+ self.assertEqual(libadobe.makeFingerprint("HelloWorld123"), b"hpp223C1kfLDOoyxo8WR7KhcXB8=", "Wrong fingerprint")
+
@patch("libadobe.Random")
def test_deviceKeyEncryption(self, random):
- '''Check if encryption with the device key works properly.'''
+ '''Check if encryption with the device key works'''
# Overwrite the get_random_bytes function that's used to get a random IV
# Forcing hard-coded IV ...
@@ -72,7 +92,7 @@ class TestAdobe(unittest.TestCase):
self.assertEqual(mock_result, expected_result, "devkey encryption returned invalid result")
def test_deviceKeyDecryption(self):
- '''Check if decryption with the device key works properly.'''
+ '''Check if decryption with the device key works'''
libadobe.devkey_bytes = bytes([0xf8, 0x7a, 0xfc, 0x8c, 0x75, 0x25, 0xdc, 0x4b, 0x83, 0xec, 0x0c, 0xe2, 0xab, 0x4b, 0xef, 0x51])
@@ -87,7 +107,7 @@ class TestAdobe(unittest.TestCase):
@freeze_time("2021-12-18 22:07:15.988961")
def test_verifyNonceCalculation(self):
- '''Check if the nonce calculation is correct, at a given date/time.'''
+ '''Check if the nonce calculation is correct, at a given date/time'''
nonce_return = libadobe.addNonce()
expected_return = "8B2aPgg6AAAAAAAA2021-12-18T22:17:15Z"
@@ -96,7 +116,7 @@ class TestAdobe(unittest.TestCase):
@freeze_time("2031-07-19 22:15:22.074860")
def test_verifyNonceCalculationFuture(self):
- '''Check if the nonce calculation is still correct in the future.'''
+ '''Check if the nonce calculation is still correct in the future'''
nonce_return = libadobe.addNonce()
expected_return = "JFUTp046AAAAAAAA2031-07-19T22:25:22Z"
@@ -105,7 +125,7 @@ class TestAdobe(unittest.TestCase):
def test_hash_node(self):
- '''Check if the XML hash (needed for the signature) is correct.'''
+ '''Check if the XML hash (needed for the signature) is correct'''
# This XML is an anonymized (all IDs replaced with random UUIDs) ACSM file.
@@ -143,12 +163,12 @@ class TestAdobe(unittest.TestCase):
mock_xml_obj = etree.fromstring(mock_xml_str)
sha_hash = libadobe.hash_node(mock_xml_obj).hexdigest().lower()
- self.assertEqual(sha_hash, "3452e3d11cdd70eb90323f291c06afafe10e098a", "Invalid SHA hash after node signing")
+ self.assertEqual(sha_hash, "3452e3d11cdd70eb90323f291c06afafe10e098a", "Invalid SHA hash for node signing")
def test_sign_node_old(self):
- '''Tests the "old" signing method with the external RSA library. '''
+ '''Check if the external RSA library (unused) signs correctly'''
# This is no longer in use by the plugin, but I still want to leave this test in,
# in case we need to go back to the old method.
@@ -168,35 +188,90 @@ class TestAdobe(unittest.TestCase):
expected_signature = "RO/JmWrustzT50GB9bSb4VdRZCP77y0ZuFFmn8gk/p0E6EbQnqP10QkB5HM8JSV9lKaKJuZpDBJ8cp+FxZmMSPe6odTUiL134Y6tXOCllBtKwVamQjsYbIFLv/HOX68rUadSHpr4QKMle2jeQinIT0viS5kwO7XofKHaSLM2XjE="
- self.assertEqual(signature, expected_signature, "Old (external RSA) signing method broken")
-
+ self.assertEqual(signature, expected_signature, "Old (external RSA) node hash signing method broken")
def test_sign_node_new(self):
- '''Tests the "new" signing method with CustomRSA. '''
+ '''Check if the builtin CustomRSA library signs correctly'''
mock_signing_key = "MIICdAIBADANBgkqhkiG9w0BAQEFAASCAl4wggJaAgEAAoGBALluuPvdDpr4L0j3eIGy3VxhgRcEKU3++qwbdvLXI99/izW9kfELFFJtq5d4ktIIUIvHsWkW0jblGi+bQ4sQXCeIvtOgqVHMSvRpW78lnGEkdD4Y1qhbcVGw7OGpWlhp8qCJKVCGbrkML7BSwFvQqqvg4vMU8O1uALfJvicKN3YfAgMBAAECf3uEg+Hr+DrstHhZF40zJPHKG3FkFd3HerXbOawMH5Q6CKTuKDGmOYQD+StFIlMArQJh8fxTVM3gSqgPkyyiesw0OuECU985FaLbUWxuCQzBcitnhl+VSv19oEPHTJWu0nYabasfT4oPjf8eiWR/ymJ9DZrjMWWy4Xf/S+/nFYUCQQDIZ1pc9nZsCB4QiBl5agTXoMcKavxFHPKxI/mHfRCHYjNyirziBJ+Dc/N40zKvldNBjO43KjLhUZs/BxdAJo09AkEA7OAdsg6SmviVV8xk0vuTmgLxhD7aZ9vpV4KF5+TH2DbximFoOP3YRObXV862wAjCpa84v43ok7Imtsu3NKQ+iwJAc0mx3GUU/1U0JoKFVSm+m2Ws27tsYT4kB/AQLvetuJSv0CcsPkI2meLsoAev0v84Ry+SIz4tgx31V672mzsSaQJBAJET1rw2Vq5Zr8Y9ZkceVFGQmfGAOW5A71Jsm6zin0+anyc874NwXaQdqiiab61/8A9gGSahOKA1DacJcCTqr28CQGm4mn3rOQFf+nniajIobATjNHaZJ76Xnc6rtoreK6+ZjO9wYF+797X/bhiV11Fpakvyrz6+t7bAd0PPQ2taTDg="
payload_bytes = bytes([0x34, 0x52, 0xe3, 0xd1, 0x1c, 0xdd, 0x70, 0xeb, 0x90, 0x32, 0x3f, 0x29, 0x1c, 0x06, 0xaf, 0xaf, 0xe1, 0x0e, 0x09, 0x8a])
- key = RSA.importKey(base64.b64decode(mock_signing_key))
-
- keylen = CustomRSA.byte_size(key.n)
- padded = CustomRSA.pad_message(payload_bytes, keylen)
- payload = CustomRSA.transform_bytes2int(padded)
- encrypted = CustomRSA.normal_encrypt(key, payload)
- block = CustomRSA.transform_int2bytes(encrypted, keylen)
+ block = CustomRSA.encrypt_for_adobe_signature(base64.b64decode(mock_signing_key), payload_bytes)
signature = base64.b64encode(block).decode()
expected_signature = "RO/JmWrustzT50GB9bSb4VdRZCP77y0ZuFFmn8gk/p0E6EbQnqP10QkB5HM8JSV9lKaKJuZpDBJ8cp+FxZmMSPe6odTUiL134Y6tXOCllBtKwVamQjsYbIFLv/HOX68rUadSHpr4QKMle2jeQinIT0viS5kwO7XofKHaSLM2XjE="
- self.assertEqual(signature, expected_signature, "CustomRSA encryption is broken")
+ self.assertEqual(signature, expected_signature, "CustomRSA node hash signing method is broken")
+
+ def test_pkcs12_extract(self):
+ '''Check if oscrypto can extract pkcs12'''
+
+ from oscrypto import keys
+ from oscrypto.asymmetric import dump_certificate, dump_private_key
+ mock_p12_data = "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"
+ mock_p12_password = b"1234"
+
+ # I don't like having to import two external libraries just to extract pkcs12 data,
+ # but I didn't find a proper Python-only solution for this,
+ # and I didn't want to implement OpenSSL interfaces on my own.
+
+ privkey, cert, _ = keys.parse_pkcs12(base64.b64decode(mock_p12_data), mock_p12_password)
+ cert = dump_certificate(cert, encoding="der")
+ privkey = dump_private_key(privkey, None, "der")
+
+ # privkey now is in PKCS#8 format. That's a wrapper around the actual PKCS#1 key.
+ # See https://tls.mbed.org/kb/cryptography/asn1-key-structures-in-der-and-pem for details.
+ # Throw away the PKCS#8 wrapper:
+ privkey = privkey[26:]
+
+ # Verify result:
+ dummy_cert_data = base64.b64decode("MIICLjCCAZegAwIBAgIUI48XmtNFINg1znympL1l1SmSYnMwDQYJKoZIhvcNAQELBQAwKTELMAkGA1UEBhMCVVMxGjAYBgNVBAMMEURlQUNTTSB1bml0IHRlc3RzMB4XDTIxMTIxOTE0MzUyMVoXDTIxMTIyOTE0MzUyMVowKTELMAkGA1UEBhMCVVMxGjAYBgNVBAMMEURlQUNTTSB1bml0IHRlc3RzMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCic4kj2kdZxLjW00l8to9hV4+gWAPJFhgvG2Io6pDR/rA1cPAR3Pu4Q4/cwab/gwmCXnnmeQwy3TyzmyCZj9tnBFeNfDsnq0TxoRoTdr0bWv0pGy1uBQ90jZVc85v2whmC9lSigueY4GlR5rOIlNsiuKBWBl/CN/6X3PaYkv04QIDAQABo1MwUTAdBgNVHQ4EFgQUiG9zxWck82kn+BoVp3So+p6tFc8wHwYDVR0jBBgwFoAUiG9zxWck82kn+BoVp3So+p6tFc8wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQCpLSNaGDRNMGAzbTtUbyWzbvgT2+hcOaqr8+fyCWtUxZ1FiPIKZEcQRQOuqZZpWft5QJSYBZ2oc3O6NFU5VrBi5UI3rTqr2S97PiJCiR4Jt1vAcpXVy6qcEjTswdpipdsN4RSeiztYk4xoqYztPoiqKJgq9nOzuXFnDKrrrMo+5w==")
+ dummy_key_data = base64.b64decode("MIICXQIBAAKBgQDCic4kj2kdZxLjW00l8to9hV4+gWAPJFhgvG2Io6pDR/rA1cPAR3Pu4Q4/cwab/gwmCXnnmeQwy3TyzmyCZj9tnBFeNfDsnq0TxoRoTdr0bWv0pGy1uBQ90jZVc85v2whmC9lSigueY4GlR5rOIlNsiuKBWBl/CN/6X3PaYkv04QIDAQABAoGAI2CIGmHyDaTG7I2X9AS752AviVJhs586ay0ZBjYtKlsWoKa/GGJmFNTckHFMjGWgs/IZNyLnOnBlbhpX5UbO1cB7r9Vk1Q3fbIeQdBySyqOG9JfZxd0n4bBSHnopL0naGA0CpSv/tVaUr0BzvNYslw5rcwVinEbGVPP6JNNqbqECQQD1Igbf2qxMHcdEA81qMdExFGmlq61W7gpKtl7XwPhtHIiXyhQsT7M996ZF64EJVg/2/6gQneZ/awJ0Aw8xDkJdAkEAyymWou2v6wPtX+X0hnXnK6OfeEfZGnExE4LpEjNTiQriabvwmCQcgHFBLxKN+C4uVK5HBlHug3jtN0jozSjcVQJBAI+ynLkJJUuRgUhbukTwYyMURkI5+2kkLaBSfBKaKoc73M6uRVkcd4Rx8mS2g3QHoWA3yjvDdGVpQ4ziZjtpknkCQQC2FSsGEYM2Xgm0hlO24xrx+K7nTXWeBk7WzuB3SHsY+yFbZG7I3KySzW5/cuC8yx8JFD1hw7LCMHJitzy3C2UVAkAQGY8PQ9u40krQekUI+imFsPSPdMZqfMKJDLwrXx0mmElUYYZBGtY0q781UYP4GArtwyusDelk6BNjVRjiWUhg")
+
+ self.assertEqual(cert, dummy_cert_data, "p12 cert invalid after extraction")
+ self.assertEqual(privkey, dummy_key_data, "p12 key invalid after extraction")
+
+ def test_pkcs12_extract_with_cryptography(self):
+ '''Check if cryptography (unused) can extract pkcs12'''
+
+ # This uses cryptography.hazmat to extract the PKCS data. That's not what the plugin is using right now.
+
+ mock_p12_data = "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"
+ mock_p12_password = b"1234"
+
+ try:
+ from cryptography.hazmat.primitives.serialization import pkcs12
+ from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
+ except:
+ return self.skipTest("cryptography not installed")
+
+
+ privkey, cert, _ = pkcs12.load_key_and_certificates(base64.b64decode(mock_p12_data), mock_p12_password)
+ privkey = privkey.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption())
+ cert = cert.public_bytes(Encoding.DER)
+
+ # privkey now is in PKCS#8 format. That's a wrapper around the actual PKCS#1 key.
+ # See https://tls.mbed.org/kb/cryptography/asn1-key-structures-in-der-and-pem for details.
+ # Throw away the PKCS#8 wrapper:
+ privkey = privkey[26:]
+
+ # Verify result:
+ dummy_cert_data = base64.b64decode("MIICLjCCAZegAwIBAgIUI48XmtNFINg1znympL1l1SmSYnMwDQYJKoZIhvcNAQELBQAwKTELMAkGA1UEBhMCVVMxGjAYBgNVBAMMEURlQUNTTSB1bml0IHRlc3RzMB4XDTIxMTIxOTE0MzUyMVoXDTIxMTIyOTE0MzUyMVowKTELMAkGA1UEBhMCVVMxGjAYBgNVBAMMEURlQUNTTSB1bml0IHRlc3RzMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCic4kj2kdZxLjW00l8to9hV4+gWAPJFhgvG2Io6pDR/rA1cPAR3Pu4Q4/cwab/gwmCXnnmeQwy3TyzmyCZj9tnBFeNfDsnq0TxoRoTdr0bWv0pGy1uBQ90jZVc85v2whmC9lSigueY4GlR5rOIlNsiuKBWBl/CN/6X3PaYkv04QIDAQABo1MwUTAdBgNVHQ4EFgQUiG9zxWck82kn+BoVp3So+p6tFc8wHwYDVR0jBBgwFoAUiG9zxWck82kn+BoVp3So+p6tFc8wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQCpLSNaGDRNMGAzbTtUbyWzbvgT2+hcOaqr8+fyCWtUxZ1FiPIKZEcQRQOuqZZpWft5QJSYBZ2oc3O6NFU5VrBi5UI3rTqr2S97PiJCiR4Jt1vAcpXVy6qcEjTswdpipdsN4RSeiztYk4xoqYztPoiqKJgq9nOzuXFnDKrrrMo+5w==")
+ dummy_key_data = base64.b64decode("MIICXQIBAAKBgQDCic4kj2kdZxLjW00l8to9hV4+gWAPJFhgvG2Io6pDR/rA1cPAR3Pu4Q4/cwab/gwmCXnnmeQwy3TyzmyCZj9tnBFeNfDsnq0TxoRoTdr0bWv0pGy1uBQ90jZVc85v2whmC9lSigueY4GlR5rOIlNsiuKBWBl/CN/6X3PaYkv04QIDAQABAoGAI2CIGmHyDaTG7I2X9AS752AviVJhs586ay0ZBjYtKlsWoKa/GGJmFNTckHFMjGWgs/IZNyLnOnBlbhpX5UbO1cB7r9Vk1Q3fbIeQdBySyqOG9JfZxd0n4bBSHnopL0naGA0CpSv/tVaUr0BzvNYslw5rcwVinEbGVPP6JNNqbqECQQD1Igbf2qxMHcdEA81qMdExFGmlq61W7gpKtl7XwPhtHIiXyhQsT7M996ZF64EJVg/2/6gQneZ/awJ0Aw8xDkJdAkEAyymWou2v6wPtX+X0hnXnK6OfeEfZGnExE4LpEjNTiQriabvwmCQcgHFBLxKN+C4uVK5HBlHug3jtN0jozSjcVQJBAI+ynLkJJUuRgUhbukTwYyMURkI5+2kkLaBSfBKaKoc73M6uRVkcd4Rx8mS2g3QHoWA3yjvDdGVpQ4ziZjtpknkCQQC2FSsGEYM2Xgm0hlO24xrx+K7nTXWeBk7WzuB3SHsY+yFbZG7I3KySzW5/cuC8yx8JFD1hw7LCMHJitzy3C2UVAkAQGY8PQ9u40krQekUI+imFsPSPdMZqfMKJDLwrXx0mmElUYYZBGtY0q781UYP4GArtwyusDelk6BNjVRjiWUhg")
+
+ self.assertEqual(cert, dummy_cert_data, "p12 cert invalid after extraction")
+ self.assertEqual(privkey, dummy_key_data, "p12 key invalid after extraction")
+
+
+
def test_Account_loginCredentialEncryption(self):
- '''Check if we can properly encrypt login credentials.'''
+ '''Check if we can properly encrypt login credentials'''
# This cert is not the actual Adobe auth cert.
# I added a fake one in here so we can also test if the encrypted data is valid
@@ -238,14 +313,15 @@ class TestOther(unittest.TestCase):
def forcefail(self):
self.assertEqual(1, 2, "force fail")
+ @unittest.skipIf(os.name == "nt", "not testing wine on Windows")
def test_wineUsernameEscaping(self):
- '''Wine uses a weird encoding for the username inside the registry. Make sure that the decoder is working correctly.'''
+ '''Check if Wine username decoder is working properly'''
self.assertEqual(fix_wine_username(r'"1234"'), b'1234', "Wine username mismatch")
self.assertEqual(fix_wine_username(r'"a\x00e931"'), b'a\xe931', "Wine username mismatch")
def test_pdf_trimEncrypt(self):
- '''Makes sure the encryption string trimming code for PDFs is working fine.'''
+ '''Check if PDF encryption string trimming code is working properly'''
input = "<<2222>]/Size 3>>AppendedData"
output = "<<2222>]/Size 3>>"
@@ -253,13 +329,13 @@ class TestOther(unittest.TestCase):
self.assertEqual(trim_encrypt_string(input), output, "PDF string trimming broken")
def test_pdf_cleanupEncrypt(self):
- '''Makes sure the encryption string cleanup / spacing code for PDFs is working fine.'''
+ '''Check if PDF encryption string spacing code is working properly'''
- self.assertEqual(cleanup_encrypt_element("ID[<1111><2222>]"), "ID[<1111> <2222>]")
- self.assertEqual(cleanup_encrypt_element("ID[ <1111> <2222>] "), "ID[<1111> <2222>]")
+ self.assertEqual(cleanup_encrypt_element("ID[<1111><2222>]"), "ID[<1111> <2222>]", "angle bracket spacing invalid")
+ self.assertEqual(cleanup_encrypt_element("ID[ <1111> <2222>] "), "ID[<1111> <2222>]", "square bracket spacing invalid")
def test_pdf_deflateCompression(self):
- '''Makes sure the deflate code for PDF rights.xml works properly.'''
+ '''Check if PDF rights.xml deflare code is working properly'''
self.assertEqual(deflate_and_base64_encode(b""), b"AwA=", "deflate code error in empty string")
self.assertEqual(deflate_and_base64_encode(b"Hello world"), b"80jNyclXKM8vykkBAA==", "deflate code error")
@@ -267,9 +343,30 @@ class TestOther(unittest.TestCase):
+# Patch to only display the docstring info, not the weird autogenerated name.
+def monkeypatch_getDescription(self, test):
+ if test.shortDescription() is not None:
+ return test.shortDescription()
+
+ return str(test)
+# Patch the error list at the end to include the autogenerated name (use original getDescription, not my override)
+def monkeypatch_printErrorList(self, flavour, errors):
+ for test, err in errors:
+ self.stream.writeln(self.separator1)
+ self.stream.writeln("%s: %s" % (flavour,self.getFullDescription(test)))
+ self.stream.writeln(self.separator2)
+ self.stream.writeln("%s" % err)
+ self.stream.flush()
if __name__ == "__main__":
- unittest.main(verbosity=1)
+ # Monkey patch the runner to get the output format I want:
+ unittest.runner.TextTestResult.getFullDescription = unittest.runner.TextTestResult.getDescription
+ unittest.runner.TextTestResult.getDescription = monkeypatch_getDescription
+ unittest.runner.TextTestResult.printErrorList = monkeypatch_printErrorList
+
+ # Run tests
+ unittest.main(verbosity=2)
+