1*cc02d7e2SAndroid Build Coastguard Worker# Copyright 2019 the gRPC authors. 2*cc02d7e2SAndroid Build Coastguard Worker# 3*cc02d7e2SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 4*cc02d7e2SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 5*cc02d7e2SAndroid Build Coastguard Worker# You may obtain a copy of the License at 6*cc02d7e2SAndroid Build Coastguard Worker# 7*cc02d7e2SAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 8*cc02d7e2SAndroid Build Coastguard Worker# 9*cc02d7e2SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 10*cc02d7e2SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 11*cc02d7e2SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*cc02d7e2SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 13*cc02d7e2SAndroid Build Coastguard Worker# limitations under the License. 14*cc02d7e2SAndroid Build Coastguard Worker"""A search algorithm over the space of all bytestrings.""" 15*cc02d7e2SAndroid Build Coastguard Worker 16*cc02d7e2SAndroid Build Coastguard Workerfrom __future__ import absolute_import 17*cc02d7e2SAndroid Build Coastguard Workerfrom __future__ import division 18*cc02d7e2SAndroid Build Coastguard Workerfrom __future__ import print_function 19*cc02d7e2SAndroid Build Coastguard Worker 20*cc02d7e2SAndroid Build Coastguard Workerimport base64 21*cc02d7e2SAndroid Build Coastguard Workerimport hashlib 22*cc02d7e2SAndroid Build Coastguard Workerimport itertools 23*cc02d7e2SAndroid Build Coastguard Workerimport logging 24*cc02d7e2SAndroid Build Coastguard Workerimport struct 25*cc02d7e2SAndroid Build Coastguard Worker 26*cc02d7e2SAndroid Build Coastguard Workerfrom examples.python.cancellation import hash_name_pb2 27*cc02d7e2SAndroid Build Coastguard Worker 28*cc02d7e2SAndroid Build Coastguard Worker_LOGGER = logging.getLogger(__name__) 29*cc02d7e2SAndroid Build Coastguard Worker_BYTE_MAX = 255 30*cc02d7e2SAndroid Build Coastguard Worker 31*cc02d7e2SAndroid Build Coastguard Worker 32*cc02d7e2SAndroid Build Coastguard Workerdef _get_hamming_distance(a, b): 33*cc02d7e2SAndroid Build Coastguard Worker """Calculates hamming distance between strings of equal length.""" 34*cc02d7e2SAndroid Build Coastguard Worker distance = 0 35*cc02d7e2SAndroid Build Coastguard Worker for char_a, char_b in zip(a, b): 36*cc02d7e2SAndroid Build Coastguard Worker if char_a != char_b: 37*cc02d7e2SAndroid Build Coastguard Worker distance += 1 38*cc02d7e2SAndroid Build Coastguard Worker return distance 39*cc02d7e2SAndroid Build Coastguard Worker 40*cc02d7e2SAndroid Build Coastguard Worker 41*cc02d7e2SAndroid Build Coastguard Workerdef _get_substring_hamming_distance(candidate, target): 42*cc02d7e2SAndroid Build Coastguard Worker """Calculates the minimum hamming distance between between the target 43*cc02d7e2SAndroid Build Coastguard Worker and any substring of the candidate. 44*cc02d7e2SAndroid Build Coastguard Worker 45*cc02d7e2SAndroid Build Coastguard Worker Args: 46*cc02d7e2SAndroid Build Coastguard Worker candidate: The string whose substrings will be tested. 47*cc02d7e2SAndroid Build Coastguard Worker target: The target string. 48*cc02d7e2SAndroid Build Coastguard Worker 49*cc02d7e2SAndroid Build Coastguard Worker Returns: 50*cc02d7e2SAndroid Build Coastguard Worker The minimum Hamming distance between candidate and target. 51*cc02d7e2SAndroid Build Coastguard Worker """ 52*cc02d7e2SAndroid Build Coastguard Worker min_distance = None 53*cc02d7e2SAndroid Build Coastguard Worker if len(target) > len(candidate): 54*cc02d7e2SAndroid Build Coastguard Worker raise ValueError("Candidate must be at least as long as target.") 55*cc02d7e2SAndroid Build Coastguard Worker for i in range(len(candidate) - len(target) + 1): 56*cc02d7e2SAndroid Build Coastguard Worker distance = _get_hamming_distance( 57*cc02d7e2SAndroid Build Coastguard Worker candidate[i : i + len(target)].lower(), target.lower() 58*cc02d7e2SAndroid Build Coastguard Worker ) 59*cc02d7e2SAndroid Build Coastguard Worker if min_distance is None or distance < min_distance: 60*cc02d7e2SAndroid Build Coastguard Worker min_distance = distance 61*cc02d7e2SAndroid Build Coastguard Worker return min_distance 62*cc02d7e2SAndroid Build Coastguard Worker 63*cc02d7e2SAndroid Build Coastguard Worker 64*cc02d7e2SAndroid Build Coastguard Workerdef _get_hash(secret): 65*cc02d7e2SAndroid Build Coastguard Worker hasher = hashlib.sha1() 66*cc02d7e2SAndroid Build Coastguard Worker hasher.update(secret) 67*cc02d7e2SAndroid Build Coastguard Worker return base64.b64encode(hasher.digest()).decode("ascii") 68*cc02d7e2SAndroid Build Coastguard Worker 69*cc02d7e2SAndroid Build Coastguard Worker 70*cc02d7e2SAndroid Build Coastguard Workerclass ResourceLimitExceededError(Exception): 71*cc02d7e2SAndroid Build Coastguard Worker """Signifies the request has exceeded configured limits.""" 72*cc02d7e2SAndroid Build Coastguard Worker 73*cc02d7e2SAndroid Build Coastguard Worker 74*cc02d7e2SAndroid Build Coastguard Workerdef _bytestrings_of_length(length): 75*cc02d7e2SAndroid Build Coastguard Worker """Generates a stream containing all bytestrings of a given length. 76*cc02d7e2SAndroid Build Coastguard Worker 77*cc02d7e2SAndroid Build Coastguard Worker Args: 78*cc02d7e2SAndroid Build Coastguard Worker length: A positive integer length. 79*cc02d7e2SAndroid Build Coastguard Worker 80*cc02d7e2SAndroid Build Coastguard Worker Yields: 81*cc02d7e2SAndroid Build Coastguard Worker All bytestrings of length `length`. 82*cc02d7e2SAndroid Build Coastguard Worker """ 83*cc02d7e2SAndroid Build Coastguard Worker for digits in itertools.product(range(_BYTE_MAX), repeat=length): 84*cc02d7e2SAndroid Build Coastguard Worker yield b"".join(struct.pack("B", i) for i in digits) 85*cc02d7e2SAndroid Build Coastguard Worker 86*cc02d7e2SAndroid Build Coastguard Worker 87*cc02d7e2SAndroid Build Coastguard Workerdef _all_bytestrings(): 88*cc02d7e2SAndroid Build Coastguard Worker """Generates a stream containing all possible bytestrings. 89*cc02d7e2SAndroid Build Coastguard Worker 90*cc02d7e2SAndroid Build Coastguard Worker This generator does not terminate. 91*cc02d7e2SAndroid Build Coastguard Worker 92*cc02d7e2SAndroid Build Coastguard Worker Yields: 93*cc02d7e2SAndroid Build Coastguard Worker All bytestrings in ascending order of length. 94*cc02d7e2SAndroid Build Coastguard Worker """ 95*cc02d7e2SAndroid Build Coastguard Worker for bytestring in itertools.chain.from_iterable( 96*cc02d7e2SAndroid Build Coastguard Worker _bytestrings_of_length(length) for length in itertools.count() 97*cc02d7e2SAndroid Build Coastguard Worker ): 98*cc02d7e2SAndroid Build Coastguard Worker yield bytestring 99*cc02d7e2SAndroid Build Coastguard Worker 100*cc02d7e2SAndroid Build Coastguard Worker 101*cc02d7e2SAndroid Build Coastguard Workerdef search( 102*cc02d7e2SAndroid Build Coastguard Worker target, 103*cc02d7e2SAndroid Build Coastguard Worker ideal_distance, 104*cc02d7e2SAndroid Build Coastguard Worker stop_event, 105*cc02d7e2SAndroid Build Coastguard Worker maximum_hashes, 106*cc02d7e2SAndroid Build Coastguard Worker interesting_hamming_distance=None, 107*cc02d7e2SAndroid Build Coastguard Worker): 108*cc02d7e2SAndroid Build Coastguard Worker """Find candidate strings. 109*cc02d7e2SAndroid Build Coastguard Worker 110*cc02d7e2SAndroid Build Coastguard Worker Search through the space of all bytestrings, in order of increasing length, 111*cc02d7e2SAndroid Build Coastguard Worker indefinitely, until a hash with a Hamming distance of `maximum_distance` or 112*cc02d7e2SAndroid Build Coastguard Worker less has been found. 113*cc02d7e2SAndroid Build Coastguard Worker 114*cc02d7e2SAndroid Build Coastguard Worker Args: 115*cc02d7e2SAndroid Build Coastguard Worker target: The search string. 116*cc02d7e2SAndroid Build Coastguard Worker ideal_distance: The desired Hamming distance. 117*cc02d7e2SAndroid Build Coastguard Worker stop_event: An event indicating whether the RPC should terminate. 118*cc02d7e2SAndroid Build Coastguard Worker maximum_hashes: The maximum number of hashes to check before stopping. 119*cc02d7e2SAndroid Build Coastguard Worker interesting_hamming_distance: If specified, strings with a Hamming 120*cc02d7e2SAndroid Build Coastguard Worker distance from the target below this value will be yielded. 121*cc02d7e2SAndroid Build Coastguard Worker 122*cc02d7e2SAndroid Build Coastguard Worker Yields: 123*cc02d7e2SAndroid Build Coastguard Worker Instances of HashNameResponse. The final entry in the stream will be of 124*cc02d7e2SAndroid Build Coastguard Worker `maximum_distance` Hamming distance or less from the target string, 125*cc02d7e2SAndroid Build Coastguard Worker while all others will be of less than `interesting_hamming_distance`. 126*cc02d7e2SAndroid Build Coastguard Worker 127*cc02d7e2SAndroid Build Coastguard Worker Raises: 128*cc02d7e2SAndroid Build Coastguard Worker ResourceLimitExceededError: If the computation exceeds `maximum_hashes` 129*cc02d7e2SAndroid Build Coastguard Worker iterations. 130*cc02d7e2SAndroid Build Coastguard Worker """ 131*cc02d7e2SAndroid Build Coastguard Worker hashes_computed = 0 132*cc02d7e2SAndroid Build Coastguard Worker for secret in _all_bytestrings(): 133*cc02d7e2SAndroid Build Coastguard Worker if stop_event.is_set(): 134*cc02d7e2SAndroid Build Coastguard Worker return 135*cc02d7e2SAndroid Build Coastguard Worker candidate_hash = _get_hash(secret) 136*cc02d7e2SAndroid Build Coastguard Worker distance = _get_substring_hamming_distance(candidate_hash, target) 137*cc02d7e2SAndroid Build Coastguard Worker if ( 138*cc02d7e2SAndroid Build Coastguard Worker interesting_hamming_distance is not None 139*cc02d7e2SAndroid Build Coastguard Worker and distance <= interesting_hamming_distance 140*cc02d7e2SAndroid Build Coastguard Worker ): 141*cc02d7e2SAndroid Build Coastguard Worker # Surface interesting candidates, but don't stop. 142*cc02d7e2SAndroid Build Coastguard Worker yield hash_name_pb2.HashNameResponse( 143*cc02d7e2SAndroid Build Coastguard Worker secret=base64.b64encode(secret), 144*cc02d7e2SAndroid Build Coastguard Worker hashed_name=candidate_hash, 145*cc02d7e2SAndroid Build Coastguard Worker hamming_distance=distance, 146*cc02d7e2SAndroid Build Coastguard Worker ) 147*cc02d7e2SAndroid Build Coastguard Worker elif distance <= ideal_distance: 148*cc02d7e2SAndroid Build Coastguard Worker # Yield ideal candidate and end the stream. 149*cc02d7e2SAndroid Build Coastguard Worker yield hash_name_pb2.HashNameResponse( 150*cc02d7e2SAndroid Build Coastguard Worker secret=base64.b64encode(secret), 151*cc02d7e2SAndroid Build Coastguard Worker hashed_name=candidate_hash, 152*cc02d7e2SAndroid Build Coastguard Worker hamming_distance=distance, 153*cc02d7e2SAndroid Build Coastguard Worker ) 154*cc02d7e2SAndroid Build Coastguard Worker return 155*cc02d7e2SAndroid Build Coastguard Worker hashes_computed += 1 156*cc02d7e2SAndroid Build Coastguard Worker if hashes_computed == maximum_hashes: 157*cc02d7e2SAndroid Build Coastguard Worker raise ResourceLimitExceededError() 158