From e163137893f3cf2cdbc9cbb435650ff922ff70ad Mon Sep 17 00:00:00 2001
From: Dmitri Bogomolov <4glitch@gmail.com>
Date: Thu, 31 Jan 2019 17:42:22 +0200
Subject: [PATCH] Added pycrypto fallback for RIPEMD160 hash function

---
 requirements.txt              |  1 +
 src/class_addressGenerator.py | 37 +++++++++++----------
 src/class_objectProcessor.py  | 18 ++++-------
 src/depends.py                | 36 ++++-----------------
 src/fallback/__init__.py      | 23 ++++++++++++++
 src/protocol.py               |  5 ++-
 src/tests/test_crypto.py      | 60 +++++++++++++++++++++++++++++++++++
 7 files changed, 118 insertions(+), 62 deletions(-)
 create mode 100644 src/tests/test_crypto.py

diff --git a/requirements.txt b/requirements.txt
index e20650d5..c55e5cf1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 python_prctl
 psutil
+pycrypto
diff --git a/src/class_addressGenerator.py b/src/class_addressGenerator.py
index a5750813..0893b73a 100644
--- a/src/class_addressGenerator.py
+++ b/src/class_addressGenerator.py
@@ -15,6 +15,7 @@ import highlevelcrypto
 from bmconfigparser import BMConfigParser
 from debug import logger
 from addresses import decodeAddress, encodeAddress, encodeVarint
+from fallback import RIPEMD160Hash
 from helper_threading import StoppableThread
 
 
@@ -133,16 +134,17 @@ class addressGenerator(threading.Thread, StoppableThread):
                     potentialPrivEncryptionKey = OpenSSL.rand(32)
                     potentialPubEncryptionKey = highlevelcrypto.pointMult(
                         potentialPrivEncryptionKey)
-                    ripe = hashlib.new('ripemd160')
                     sha = hashlib.new('sha512')
                     sha.update(
                         potentialPubSigningKey + potentialPubEncryptionKey)
-                    ripe.update(sha.digest())
-                    if ripe.digest()[:numberOfNullBytesDemandedOnFrontOfRipeHash] == '\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash:
+                    ripe = RIPEMD160Hash(sha.digest()).digest()
+                    if (
+                        ripe[:numberOfNullBytesDemandedOnFrontOfRipeHash] ==
+                        '\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash
+                    ):
                         break
                 logger.info(
-                    'Generated address with ripe digest: %s',
-                    hexlify(ripe.digest()))
+                    'Generated address with ripe digest: %s', hexlify(ripe))
                 try:
                     logger.info(
                         'Address generator calculated %s addresses at %s'
@@ -156,7 +158,7 @@ class addressGenerator(threading.Thread, StoppableThread):
                     # time.time() - startTime equaled zero.
                     pass
                 address = encodeAddress(
-                    addressVersionNumber, streamNumber, ripe.digest())
+                    addressVersionNumber, streamNumber, ripe)
 
                 # An excellent way for us to store our keys
                 # is in Wallet Import Format. Let us convert now.
@@ -203,7 +205,7 @@ class addressGenerator(threading.Thread, StoppableThread):
                 shared.reloadMyAddressHashes()
                 if addressVersionNumber == 3:
                     queues.workerQueue.put((
-                        'sendOutOrStoreMyV3Pubkey', ripe.digest()))
+                        'sendOutOrStoreMyV3Pubkey', ripe))
                 elif addressVersionNumber == 4:
                     queues.workerQueue.put((
                         'sendOutOrStoreMyV4Pubkey', address))
@@ -255,17 +257,18 @@ class addressGenerator(threading.Thread, StoppableThread):
                             potentialPrivEncryptionKey)
                         signingKeyNonce += 2
                         encryptionKeyNonce += 2
-                        ripe = hashlib.new('ripemd160')
                         sha = hashlib.new('sha512')
                         sha.update(
                             potentialPubSigningKey + potentialPubEncryptionKey)
-                        ripe.update(sha.digest())
-                        if ripe.digest()[:numberOfNullBytesDemandedOnFrontOfRipeHash] == '\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash:
+                        ripe = RIPEMD160Hash(sha.digest()).digest()
+                        if (
+                            ripe[:numberOfNullBytesDemandedOnFrontOfRipeHash] ==
+                            '\x00' * numberOfNullBytesDemandedOnFrontOfRipeHash
+                        ):
                             break
 
                     logger.info(
-                        'Generated address with ripe digest: %s',
-                        hexlify(ripe.digest()))
+                        'Generated address with ripe digest: %s', hexlify(ripe))
                     try:
                         logger.info(
                             'Address generator calculated %s addresses'
@@ -280,7 +283,7 @@ class addressGenerator(threading.Thread, StoppableThread):
                         # time.time() - startTime equaled zero.
                         pass
                     address = encodeAddress(
-                        addressVersionNumber, streamNumber, ripe.digest())
+                        addressVersionNumber, streamNumber, ripe)
 
                     saveAddressToDisk = True
                     # If we are joining an existing chan, let us check
@@ -357,13 +360,13 @@ class addressGenerator(threading.Thread, StoppableThread):
                             ))
                             listOfNewAddressesToSendOutThroughTheAPI.append(
                                 address)
-                            shared.myECCryptorObjects[ripe.digest()] = \
+                            shared.myECCryptorObjects[ripe] = \
                                 highlevelcrypto.makeCryptor(
                                 hexlify(potentialPrivEncryptionKey))
-                            shared.myAddressesByHash[ripe.digest()] = address
+                            shared.myAddressesByHash[ripe] = address
                             tag = hashlib.sha512(hashlib.sha512(
                                 encodeVarint(addressVersionNumber) +
-                                encodeVarint(streamNumber) + ripe.digest()
+                                encodeVarint(streamNumber) + ripe
                             ).digest()).digest()[32:]
                             shared.myAddressesByTag[tag] = address
                             if addressVersionNumber == 3:
@@ -371,7 +374,7 @@ class addressGenerator(threading.Thread, StoppableThread):
                                 # the worker thread won't send out
                                 # the pubkey over the network.
                                 queues.workerQueue.put((
-                                    'sendOutOrStoreMyV3Pubkey', ripe.digest()))
+                                    'sendOutOrStoreMyV3Pubkey', ripe))
                             elif addressVersionNumber == 4:
                                 queues.workerQueue.put((
                                     'sendOutOrStoreMyV4Pubkey', address))
diff --git a/src/class_objectProcessor.py b/src/class_objectProcessor.py
index 87b8eb21..62713b7b 100644
--- a/src/class_objectProcessor.py
+++ b/src/class_objectProcessor.py
@@ -24,6 +24,7 @@ import queues
 import state
 import tr
 from debug import logger
+from fallback import RIPEMD160Hash
 import l10n
 
 
@@ -288,9 +289,7 @@ class objectProcessor(threading.Thread):
             sha = hashlib.new('sha512')
             sha.update(
                 '\x04' + publicSigningKey + '\x04' + publicEncryptionKey)
-            ripeHasher = hashlib.new('ripemd160')
-            ripeHasher.update(sha.digest())
-            ripe = ripeHasher.digest()
+            ripe = RIPEMD160Hash(sha.digest()).digest()
 
             logger.debug(
                 'within recpubkey, addressVersion: %s, streamNumber: %s'
@@ -354,9 +353,7 @@ class objectProcessor(threading.Thread):
 
             sha = hashlib.new('sha512')
             sha.update(publicSigningKey + publicEncryptionKey)
-            ripeHasher = hashlib.new('ripemd160')
-            ripeHasher.update(sha.digest())
-            ripe = ripeHasher.digest()
+            ripe = RIPEMD160Hash(sha.digest()).digest()
 
             logger.debug(
                 'within recpubkey, addressVersion: %s, streamNumber: %s'
@@ -575,10 +572,9 @@ class objectProcessor(threading.Thread):
         # calculate the fromRipe.
         sha = hashlib.new('sha512')
         sha.update(pubSigningKey + pubEncryptionKey)
-        ripe = hashlib.new('ripemd160')
-        ripe.update(sha.digest())
+        ripe = RIPEMD160Hash(sha.digest()).digest()
         fromAddress = encodeAddress(
-            sendersAddressVersionNumber, sendersStreamNumber, ripe.digest())
+            sendersAddressVersionNumber, sendersStreamNumber, ripe)
 
         # Let's store the public key in case we want to reply to this
         # person.
@@ -897,9 +893,7 @@ class objectProcessor(threading.Thread):
 
         sha = hashlib.new('sha512')
         sha.update(sendersPubSigningKey + sendersPubEncryptionKey)
-        ripeHasher = hashlib.new('ripemd160')
-        ripeHasher.update(sha.digest())
-        calculatedRipe = ripeHasher.digest()
+        calculatedRipe = RIPEMD160Hash(sha.digest()).digest()
 
         if broadcastVersion == 4:
             if toRipe != calculatedRipe:
diff --git a/src/depends.py b/src/depends.py
index 9a2e2158..53bd0184 100755
--- a/src/depends.py
+++ b/src/depends.py
@@ -162,37 +162,13 @@ def try_import(module, log_extra=False):
         return False
 
 
-# We need to check hashlib for RIPEMD-160, as it won't be available
-# if OpenSSL is not linked against or the linked OpenSSL has RIPEMD
-# disabled.
-def check_hashlib():
-    """Do hashlib check.
-
-    The hashlib module check with version as if it included or not
-    in The Python Standard library, it's a module containing an
-    interface to the most popular hashing algorithms. hashlib
-    implements some of the algorithms, however if  OpenSSL
-    installed, hashlib is able to use this algorithms as well.
-    """
-    if sys.hexversion < 0x020500F0:
-        logger.error(
-            'The hashlib module is not included in this version of Python.')
-        return False
-    import hashlib
-    if '_hashlib' not in hashlib.__dict__:
-        logger.error(
-            'The RIPEMD-160 hash algorithm is not available.'
-            ' The hashlib module is not linked against OpenSSL.')
-        return False
+def check_ripemd160():
+    """Check availability of the RIPEMD160 hash function"""
     try:
-        hashlib.new('ripemd160')
-    except ValueError:
-        logger.error(
-            'The RIPEMD-160 hash algorithm is not available.'
-            ' The hashlib module utilizes an OpenSSL library with'
-            ' RIPEMD disabled.')
+        from fallback import RIPEMD160Hash
+    except ImportError:
         return False
-    return True
+    return RIPEMD160Hash is not None
 
 
 def check_sqlite():
@@ -446,7 +422,7 @@ def check_dependencies(verbose=False, optional=False):
             ' or greater is required.')
         has_all_dependencies = False
 
-    check_functions = [check_hashlib, check_sqlite, check_openssl]
+    check_functions = [check_ripemd160, check_sqlite, check_openssl]
     if optional:
         check_functions.extend([check_msgpack, check_pyqt, check_curses])
 
diff --git a/src/fallback/__init__.py b/src/fallback/__init__.py
index 9ac081b6..d45c754d 100644
--- a/src/fallback/__init__.py
+++ b/src/fallback/__init__.py
@@ -1,3 +1,26 @@
 """
 .. todo:: hello world
 """
+
+import hashlib
+
+# We need to check hashlib for RIPEMD-160, as it won't be available
+# if OpenSSL is not linked against or the linked OpenSSL has RIPEMD
+# disabled.
+
+try:
+    hashlib.new('ripemd160')
+except ValueError:
+    try:
+        from Crypto.Hash import RIPEMD
+    except ImportError:
+        RIPEMD160Hash = None
+    else:
+        RIPEMD160Hash = RIPEMD.RIPEMD160Hash
+else:
+    def RIPEMD160Hash(data=None):
+        """hashlib based RIPEMD160Hash"""
+        hasher = hashlib.new('ripemd160')
+        if data:
+            hasher.update(data)
+        return hasher
diff --git a/src/protocol.py b/src/protocol.py
index fd37dc44..6f89c186 100644
--- a/src/protocol.py
+++ b/src/protocol.py
@@ -25,6 +25,7 @@ from addresses import (
     encodeVarint, decodeVarint, decodeAddress, varintDecodeError)
 from bmconfigparser import BMConfigParser
 from debug import logger
+from fallback import RIPEMD160Hash
 from helper_sql import sqlExecute
 from version import softwareVersion
 
@@ -411,9 +412,7 @@ def decryptAndCheckPubkeyPayload(data, address):
 
         sha = hashlib.new('sha512')
         sha.update(publicSigningKey + publicEncryptionKey)
-        ripeHasher = hashlib.new('ripemd160')
-        ripeHasher.update(sha.digest())
-        embeddedRipe = ripeHasher.digest()
+        embeddedRipe = RIPEMD160Hash(sha.digest()).digest()
 
         if embeddedRipe != ripe:
             # Although this pubkey object had the tag were were looking for
diff --git a/src/tests/test_crypto.py b/src/tests/test_crypto.py
new file mode 100644
index 00000000..7410e958
--- /dev/null
+++ b/src/tests/test_crypto.py
@@ -0,0 +1,60 @@
+"""
+Test the alternatives for crypto primitives
+"""
+
+import hashlib
+import unittest
+from abc import ABCMeta, abstractmethod
+from binascii import hexlify, unhexlify
+
+try:
+    from Crypto.Hash import RIPEMD
+except ImportError:
+    RIPEMD = None
+
+
+# These keys are from addresses test script
+sample_pubsigningkey = unhexlify(
+    '044a367f049ec16cb6b6118eb734a9962d10b8db59c890cd08f210c43ff08bdf09d'
+    '16f502ca26cd0713f38988a1237f1fc8fa07b15653c996dc4013af6d15505ce')
+sample_pubencryptionkey = unhexlify(
+    '044597d59177fc1d89555d38915f581b5ff2286b39d022ca0283d2bdd5c36be5d3c'
+    'e7b9b97792327851a562752e4b79475d1f51f5a71352482b241227f45ed36a9')
+
+sample_ripe = '003cd097eb7f35c87b5dc8b4538c22cb55312a9f'
+
+_sha = hashlib.new('sha512')
+_sha.update(sample_pubsigningkey + sample_pubencryptionkey)
+
+pubkey_sha = _sha.digest()
+
+
+class RIPEMD160TestCase(object):
+    """Base class for RIPEMD160 test case"""
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def _hashdigest(self, data):
+        """RIPEMD160 digest implementation"""
+        pass
+
+    def test_hash_string(self):
+        """Check RIPEMD160 hash function on string"""
+        self.assertEqual(hexlify(self._hashdigest(pubkey_sha)), sample_ripe)
+
+
+class TestHashlib(RIPEMD160TestCase, unittest.TestCase):
+    """RIPEMD160 test case for hashlib"""
+    @staticmethod
+    def _hashdigest(data):
+        hasher = hashlib.new('ripemd160')
+        hasher.update(data)
+        return hasher.digest()
+
+
+@unittest.skipUnless(RIPEMD, 'pycrypto package not found')
+class TestCrypto(RIPEMD160TestCase, unittest.TestCase):
+    """RIPEMD160 test case for Crypto"""
+    @staticmethod
+    def _hashdigest(data):
+        return RIPEMD.RIPEMD160Hash(data).digest()