Repository: microsoft/presidio

Issue:
crypto_recognizer throws an exception

When calling the engine analyze API  like

```
 response = engine.analyze(correlation_id=0,
                                      text=text_to_analyze,
                                      language='en',
                                      entities=[],
                                      all_fields=True,
                                      score_threshold=0.5)
```

and the value of  'text_to_analyze' is 

"/boardingPass/v1/devices/34e7b5e1a0aa1d6f3d862b52a289cdb7/registrations/pass.apoc.wallet/"

The exception below is thrown


` File "/home/folder_name/presidio_testing/my_venv/lib/python3.6/site-packages/analyzer/analyzer_engine.py", line 204, in analyze
    current_results = recognizer.analyze(text, entities, nlp_artifacts)
  File "/home/folder_name/presidio_testing/my_venv/lib/python3.6/site-packages/analyzer/pattern_recognizer.py", line 61, in analyze
    pattern_result = self.__analyze_patterns(text)
  File "/home/folder_name/presidio_testing/my_venv/lib/python3.6/site-packages/analyzer/pattern_recognizer.py", line 144, in __analyze_patterns
    validation_result = self.validate_result(current_match)
  File "/home/folder_name/presidio_testing/my_venv/lib/python3.6/site-packages/analyzer/predefined_recognizers/crypto_recognizer.py", line 23, in validate_result
    bcbytes = CryptoRecognizer.__decode_base58(pattern_text, 25)
  File "/home/folder_name/presidio_testing/my_venv/lib/python3.6/site-packages/analyzer/predefined_recognizers/crypto_recognizer.py", line 33, in __decode_base58
    n = n * 58 + digits58.index(char)`

ValueError: substring not found


Discussion:
Hi @balderman,
Which regex engine are you using? pyre2 or regex?
I am using the python builtin regex (not re2)
We're looking into it.
I tried to get into the code and see what caused the bug. 
Here are my understandings:
PatternRecognizer comes with an 'abstract' method `def validate_result(self, pattern_text):`.
According to the method doc, this method is called after there was a match against the RegExp.
The implementation of CryptoRecognizer comes with an assumption that the matched string  contains only chars that are defined in `digits58`. This is a wrong assumption for some of the inputs that creates a match against the RegExp.
So I think we have 2 options here:

1.  The RegExp creates a false match  and this is the part that should be fixed
OR
2.  The RegExp creates a true match but the assumption that the matched string must contain only chars that are defined in `digits58` is wrong.


```
    def validate_result(self, pattern_text):
        # try:
        bcbytes = CryptoRecognizer.__decode_base58(pattern_text, 25)
        result = bcbytes[-4:] == sha256(sha256(bcbytes[:-4])
                                        .digest()).digest()[:4]
        return result

    @staticmethod
    def __decode_base58(bc, length):
        digits58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
        n = 0
        for char in bc:
            n = n * 58 + digits58.index(char) # Exception is thrown here since the char in not in digits58 
        return n.to_bytes(length, 'big')

```
Below is a code that reproduce the issue.

**The bottom line is that 0 is not included in digits58  but it is a part of the matched string**

```
import re

REGEX = r'\b[13][a-km-zA-HJ-NP-Z0-9]{26,33}\b'
input_str = '/boardingPass/v1/devices/34e7b5e1a0aa1d6f3d862b52a289cdb7/registrations/pass.apoc.wallet/'


def __decode_base58(bc):
    digits58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
    n = 0
    for char in bc:
        print('current char is: {}'.format(char))
        try:
            n = n * 58 + digits58.index(char)
        except ValueError as e:
            print('Exception was thrown for char {}. It means that the char is "{}" is not a char in "digits58"'.format(
                char, char))


matches = re.finditer(
    REGEX,
    input_str,
    flags=re.IGNORECASE | re.DOTALL | re.MULTILINE)

for match in matches:
    start, end = match.span()
    current_match = input_str[start:end]
    print('\t Match:' + current_match)
    __decode_base58(current_match)
print('done')

```

output

```
	 Match:34e7b5e1a0aa1d6f3d862b52a289cdb7
current char is: 3
current char is: 4
current char is: e
current char is: 7
current char is: b
current char is: 5
current char is: e
current char is: 1
current char is: a
current char is: 0
Exception was thrown for char 0. It means that the char is "0" is not a char in "digits58"
current char is: a
current char is: a
current char is: 1
current char is: d
current char is: 6
current char is: f
current char is: 3
current char is: d
current char is: 8
current char is: 6
current char is: 2
current char is: b
current char is: 5
current char is: 2
current char is: a
current char is: 2
current char is: 8
current char is: 9
current char is: c
current char is: d
current char is: b
current char is: 7
done
```

My current (brutal..) workaround is

```
engine = AnalyzerEngine()
del engine.registry.recognizers[1] # CryptoRecognizer is the second item in the list
```


Resolving patch:
diff --git a/presidio-analyzer/analyzer/predefined_recognizers/crypto_recognizer.py b/presidio-analyzer/analyzer/predefined_recognizers/crypto_recognizer.py
--- a/presidio-analyzer/analyzer/predefined_recognizers/crypto_recognizer.py
+++ b/presidio-analyzer/analyzer/predefined_recognizers/crypto_recognizer.py
@@ -4,7 +4,7 @@
 
 # Copied from:
 # http://rosettacode.org/wiki/Bitcoin/address_validation#Python
-REGEX = r'\b[13][a-km-zA-HJ-NP-Z0-9]{26,33}\b'
+REGEX = r'\b[13][a-km-zA-HJ-NP-Z1-9]{26,33}\b'
 CONTEXT = ["wallet", "btc", "bitcoin", "crypto"]
 
 
@@ -19,11 +19,12 @@ def __init__(self):
                          context=CONTEXT)
 
     def validate_result(self, pattern_text):
-        # try:
-        bcbytes = CryptoRecognizer.__decode_base58(pattern_text, 25)
-        result = bcbytes[-4:] == sha256(sha256(bcbytes[:-4])
-                                        .digest()).digest()[:4]
-        return result
+        try:
+            bcbytes = CryptoRecognizer.__decode_base58(pattern_text, 25)
+            return bcbytes[-4:] == sha256(sha256(bcbytes[:-4])
+                                          .digest()).digest()[:4]
+        except ValueError:
+            return False
 
     @staticmethod
     def __decode_base58(bc, length):


Test patch:
diff --git a/presidio-analyzer/tests/test_crypto_recognizer.py b/presidio-analyzer/tests/test_crypto_recognizer.py
--- a/presidio-analyzer/tests/test_crypto_recognizer.py
+++ b/presidio-analyzer/tests/test_crypto_recognizer.py
@@ -21,8 +21,7 @@ def test_valid_btc(self):
 
     def test_valid_btc_with_exact_context(self):
         wallet = '16Yeky6GMjeNkAiNcBY7ZhrLoMSgg1BoyZ'
-        context = 'my wallet address is: '
-        results = crypto_recognizer.analyze(context + wallet, entities)
+        results = crypto_recognizer.analyze('my wallet address is: ' + wallet, entities)
 
         assert len(results) == 1
         assert_result(results[0], entities[0], 22, 56, EntityRecognizer.MAX_SCORE)
@@ -32,3 +31,9 @@ def test_invalid_btc(self):
         results = crypto_recognizer.analyze('my wallet address is ' + wallet, entities)
 
         assert len(results) == 0
+
+    def test_invalid_btc_chars(self):
+        wallet = '34e7b5e1a0aa1d6f3d862b52a289cdb7'
+        results = crypto_recognizer.analyze('my wallet address is ' + wallet, entities)
+
+        assert len(results) == 0
\ No newline at end of file