xref: /aosp_15_r20/external/grpc-grpc/examples/python/cancellation/search.py (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
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