Repository: caikit/caikit
Pull request #304 (2023-07-07 17:15:13)

Reversible Hasher
This will be used to obfuscate the trainer name in the ID given when submitting a training

Supports #308 


Patch:
diff --git a/caikit/core/toolkit/reversible_hasher.py b/caikit/core/toolkit/reversible_hasher.py
new file mode 100644
index 00000000..5e42e274
--- /dev/null
+++ b/caikit/core/toolkit/reversible_hasher.py
@@ -0,0 +1,50 @@
+# Copyright The Caikit Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""The ReversibleHasher provides a simple obfuscation tool for munging strings
+in a reversible way. It is useful when needing a repeatable way to obfuscate a
+string that may come from a configuration detail that should not be
+transparently visible to a user, but needs to be trivially reversed, possibly
+with entirely new in-memory state.
+
+WARNING: This utility is intentionally not a cryptographically secure hash! It
+    is explicitly designed to be reversible.
+"""
+
+# Standard
+import random
+import string
+
+# Create a statically seeded instance of Random to perform shuffling in a
+# consistent way
+_RAND_INST = random.Random()
+_RAND_INST.seed(42)
+_CHARSET = string.ascii_letters + string.digits + "-_"
+_SHUFFLED = list(_CHARSET)
+_RAND_INST.shuffle(_SHUFFLED)
+
+# Create the static forward/backward maps for the character set
+FORWARD_MAP = dict(zip(_CHARSET, _SHUFFLED))
+BACKWARD_MAP = {v: k for k, v in FORWARD_MAP.items()}
+
+
+class ReversibleHasher:
+    __doc__ = __doc__
+
+    @classmethod
+    def hash(cls, val: str):
+        return "".join([FORWARD_MAP.get(ch, ch) for ch in val])
+
+    @classmethod
+    def reverse_hash(cls, hash_val: str):
+        return "".join([BACKWARD_MAP.get(ch, ch) for ch in hash_val])


Test patch:
diff --git a/tests/core/toolkit/test_reversible_hasher.py b/tests/core/toolkit/test_reversible_hasher.py
new file mode 100644
index 00000000..28cd3151
--- /dev/null
+++ b/tests/core/toolkit/test_reversible_hasher.py
@@ -0,0 +1,82 @@
+# Copyright The Caikit Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Tests for the reversible hasher
+"""
+
+# Standard
+from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
+import random
+
+# Local
+from caikit.core.toolkit.reversible_hasher import ReversibleHasher
+
+## Helpers #####################################################################
+
+
+def do_hash(x):
+    hashed = ReversibleHasher.hash(x)
+    reversed_ = ReversibleHasher.reverse_hash(hashed)
+    return (hashed, reversed_)
+
+
+## Tests #######################################################################
+
+
+def test_reversible_hasher_single_process():
+    """Make sure that hashing the same string multiple times in sequence is
+    repeatable and reversible
+    """
+    sample_string = "thisisatest1234-of_stuff"
+    hashed1 = ReversibleHasher.hash(sample_string)
+    hashed2 = ReversibleHasher.hash(sample_string)
+    reversed1 = ReversibleHasher.reverse_hash(hashed1)
+    reversed2 = ReversibleHasher.reverse_hash(hashed2)
+    assert hashed1 == hashed2
+    assert reversed1 == reversed2 == sample_string
+
+
+def test_reversible_hasher_multi_thread():
+    """Make sure that hashing the same string multiple times in different
+    threads is repeatable and reversible
+    """
+    sample_string = "ANoTHe-r_t327"
+    executor = ThreadPoolExecutor()
+    future1 = executor.submit(do_hash, x=sample_string)
+    future2 = executor.submit(do_hash, x=sample_string)
+    (hashed1, reversed1) = future1.result()
+    (hashed2, reversed2) = future2.result()
+    assert hashed1 == hashed2
+    assert reversed1 == reversed2 == sample_string
+
+
+def test_reversible_hasher_multi_process():
+    """Make sure that hashing the same string multiple times in different
+    processes is repeatable and reversible
+    """
+    sample_string = "y3t_ANoTHe-r_tES T"
+    executor = ProcessPoolExecutor()
+    future1 = executor.submit(do_hash, x=sample_string)
+    future2 = executor.submit(do_hash, x=sample_string)
+    (hashed1, reversed1) = future1.result()
+    (hashed2, reversed2) = future2.result()
+    assert hashed1 == hashed2
+    assert reversed1 == reversed2 == sample_string
+
+
+def test_reversible_hasher_random_still_random():
+    """Make sure that the random state is not changed by hashing"""
+    rand_state = random.getstate()
+    do_hash("yetanother")
+    assert random.getstate() == rand_state