humanized-hash 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. humanized_hash-1.0.0/CHANGELOG.md +46 -0
  2. humanized_hash-1.0.0/CONTRIBUTING.md +72 -0
  3. humanized_hash-1.0.0/LICENSE +21 -0
  4. humanized_hash-1.0.0/MANIFEST.in +7 -0
  5. humanized_hash-1.0.0/PKG-INFO +191 -0
  6. humanized_hash-1.0.0/README.md +158 -0
  7. humanized_hash-1.0.0/docs/INTEGRATION.md +221 -0
  8. humanized_hash-1.0.0/docs/images/sui-a.png +0 -0
  9. humanized_hash-1.0.0/docs/images/sui-b.png +0 -0
  10. humanized_hash-1.0.0/docs/images/sui-c.png +0 -0
  11. humanized_hash-1.0.0/pyproject.toml +61 -0
  12. humanized_hash-1.0.0/setup.cfg +4 -0
  13. humanized_hash-1.0.0/src/humanized_hash/__init__.py +56 -0
  14. humanized_hash-1.0.0/src/humanized_hash/__main__.py +10 -0
  15. humanized_hash-1.0.0/src/humanized_hash/_api.py +388 -0
  16. humanized_hash-1.0.0/src/humanized_hash/_bmp.py +38 -0
  17. humanized_hash-1.0.0/src/humanized_hash/_cli.py +514 -0
  18. humanized_hash-1.0.0/src/humanized_hash/_contrast.py +71 -0
  19. humanized_hash-1.0.0/src/humanized_hash/_derive.py +145 -0
  20. humanized_hash-1.0.0/src/humanized_hash/_errors.py +63 -0
  21. humanized_hash-1.0.0/src/humanized_hash/_flatten.py +31 -0
  22. humanized_hash-1.0.0/src/humanized_hash/_image.py +162 -0
  23. humanized_hash-1.0.0/src/humanized_hash/_jpeg.py +360 -0
  24. humanized_hash-1.0.0/src/humanized_hash/_model.py +185 -0
  25. humanized_hash-1.0.0/src/humanized_hash/_png.py +184 -0
  26. humanized_hash-1.0.0/src/humanized_hash/_raster.py +396 -0
  27. humanized_hash-1.0.0/src/humanized_hash/_version.py +5 -0
  28. humanized_hash-1.0.0/src/humanized_hash/py.typed +0 -0
  29. humanized_hash-1.0.0/src/humanized_hash.egg-info/PKG-INFO +191 -0
  30. humanized_hash-1.0.0/src/humanized_hash.egg-info/SOURCES.txt +70 -0
  31. humanized_hash-1.0.0/src/humanized_hash.egg-info/dependency_links.txt +1 -0
  32. humanized_hash-1.0.0/src/humanized_hash.egg-info/entry_points.txt +2 -0
  33. humanized_hash-1.0.0/src/humanized_hash.egg-info/top_level.txt +1 -0
  34. humanized_hash-1.0.0/testdata/SOURCE +26 -0
  35. humanized_hash-1.0.0/testdata/golden/evm-1-keyed-128-dark.png +0 -0
  36. humanized_hash-1.0.0/testdata/golden/evm-1-keyed-128.png +0 -0
  37. humanized_hash-1.0.0/testdata/golden/evm-1-keyed-48.png +0 -0
  38. humanized_hash-1.0.0/testdata/golden/evm-1-universal-128.png +0 -0
  39. humanized_hash-1.0.0/testdata/golden/evm-1-universal-256-transparent.png +0 -0
  40. humanized_hash-1.0.0/testdata/golden/evm-1-universal-32.png +0 -0
  41. humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-brackets.png +0 -0
  42. humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-chamfered.png +0 -0
  43. humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-double.png +0 -0
  44. humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-round-double.png +0 -0
  45. humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-round-gaps.png +0 -0
  46. humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-round-thick.png +0 -0
  47. humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-round-ticks.png +0 -0
  48. humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-thick.png +0 -0
  49. humanized_hash-1.0.0/testdata/golden/evm-2-universal-128-plain.png +0 -0
  50. humanized_hash-1.0.0/testdata/golden/evm-2-universal-128-round-plain.png +0 -0
  51. humanized_hash-1.0.0/testdata/golden/evm-2-universal-128-round.png +0 -0
  52. humanized_hash-1.0.0/testdata/golden/poison-a-universal-128.png +0 -0
  53. humanized_hash-1.0.0/testdata/golden/poison-b-universal-128.png +0 -0
  54. humanized_hash-1.0.0/testdata/golden/sampler-universal-128.png +0 -0
  55. humanized_hash-1.0.0/testdata/vectors.tsv +271 -0
  56. humanized_hash-1.0.0/tests/__init__.py +13 -0
  57. humanized_hash-1.0.0/tests/decoders.py +225 -0
  58. humanized_hash-1.0.0/tests/reference.py +209 -0
  59. humanized_hash-1.0.0/tests/support.py +67 -0
  60. humanized_hash-1.0.0/tests/test_api.py +706 -0
  61. humanized_hash-1.0.0/tests/test_cli.py +456 -0
  62. humanized_hash-1.0.0/tests/test_decoders.py +241 -0
  63. humanized_hash-1.0.0/tests/test_docs.py +87 -0
  64. humanized_hash-1.0.0/tests/test_package.py +134 -0
  65. humanized_hash-1.0.0/tests/test_primitives.py +167 -0
  66. humanized_hash-1.0.0/tests/test_raster.py +125 -0
  67. humanized_hash-1.0.0/tests/test_robustness.py +145 -0
  68. humanized_hash-1.0.0/tests/test_vectors.py +177 -0
  69. humanized_hash-1.0.0/tools/bench.py +78 -0
  70. humanized_hash-1.0.0/tools/crosscheck.sh +48 -0
  71. humanized_hash-1.0.0/tools/edge-cases.txt +166 -0
  72. humanized_hash-1.0.0/tools/update-vectors.sh +45 -0
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Each release names the hh-cpp release
6
+ its golden vectors were copied from. The algorithm itself is frozen and has no version: no
7
+ release changes a fingerprint, a pixel or an encoded byte.
8
+
9
+ ## [1.0.0] - 2026-09-21
10
+
11
+ The first release. Golden vectors: hh-cpp v1.0.0.
12
+
13
+ ### Added
14
+
15
+ - Public API in `humanized_hash` (type hints, `py.typed`, docstrings): `BaseDigest.of`, `of_hex`
16
+ and `of_text` compute the base digest of bytes, hexadecimal or text, and `from_bytes` restores
17
+ a stored one; `SecretKey`, a context manager that wipes its copy of the key, with `closed` and
18
+ a key check value that stays readable after the key is closed; universal and keyed
19
+ `Fingerprint` with layout and six-character tag, and `from_bytes` for a fingerprint computed
20
+ elsewhere; `RenderOptions` with square and round shapes, keyed-mode frame markers, any
21
+ background colour and transparency, frame transparency and a WCAG contrast measure; `Image`
22
+ with RGBA pixels, PNG, BMP and JPEG encoders and `save`. `HhError`, a `ValueError`, carries the
23
+ `ErrorCode` of the specification; arguments of the wrong type raise `TypeError`. Values are
24
+ immutable, hashable and picklable with every protocol; a key is neither picklable nor
25
+ printable, and neither is the buffer that holds it.
26
+ - A rasteriser that evaluates whole rows of samples as integer bit sets, exact to the per-sample
27
+ definition of the specification; fixed-Huffman deflate, PNG, BMP and a baseline JPEG encoder
28
+ written in the library; Adler-32; PBKDF2 over `hashlib.sha256` for interpreters without
29
+ OpenSSL. SHA-256, HMAC, PBKDF2 and CRC-32 come from the standard library. No dependencies;
30
+ integer arithmetic only.
31
+ - The command `humanized-hash` (`python -m humanized_hash`) with the options of `hh_cli` of
32
+ hh-cpp, and with `--batch`, which follows the batch format of `hh_cli` to the letter, and
33
+ `--generate` for differential tests.
34
+ - Tests with `unittest`: known-answer tests (FIPS 180-4, RFC 4231, RFC 7914, CRC-32 and Adler-32
35
+ check values); the golden vectors and golden files of hh-cpp; the rasteriser against a
36
+ per-sample transcription of the specification for every size from 16 to 96 and samples up to
37
+ 1024; the deflate stream against a literal transcription of its matching rule; decoders for
38
+ PNG (`zlib`), BMP and baseline JPEG; error order, API and robustness tests; source rules
39
+ (no floating point, standard library only, documented API).
40
+ - `tools/crosscheck.sh`: differential test against `hh_cli` of hh-cpp with pseudo-random cases
41
+ and the hand-made cases of `tools/edge-cases.txt`. `tools/update-vectors.sh` copies the vectors
42
+ and writes `testdata/SOURCE`. `tools/bench.py` times every step.
43
+ - Packaging with setuptools: `humanized-hash` on PyPI, CPython 3.9 to 3.14 and PyPy 3.10; the
44
+ source distribution carries the tests and the vectors, the wheel the package only.
45
+
46
+ [1.0.0]: https://github.com/censync/hh-python/releases/tag/v1.0.0
@@ -0,0 +1,72 @@
1
+ # Contributing to hh-python
2
+
3
+ hh-python is the Python implementation of Humanized Hash (`hh`). The C++17 repository hh-cpp is
4
+ the reference: it owns `docs/SPEC.md`, `docs/SECURITY.md` and the canonical golden vectors. This
5
+ repository carries a byte-identical copy of the vectors in `testdata/` and records the hh-cpp
6
+ release and the file hashes in `testdata/SOURCE`. The port follows `SPEC.md`, not the C++ source.
7
+
8
+ Bug reports and patches are welcome: open an issue or a pull request. Security problems are
9
+ reported privately, as `docs/SECURITY.md` of hh-cpp describes.
10
+
11
+ ## The algorithm is frozen
12
+
13
+ Output is byte-identical with hh-cpp. A mismatch against the vectors is a bug here, never a reason
14
+ to change the vectors. The algorithm has no version and never changes; what the specification
15
+ leaves open (API shape, error texts, performance) may evolve under SemVer.
16
+
17
+ hh is a standalone library. Nothing here names a particular host application.
18
+
19
+ ## Dependencies and license
20
+
21
+ - `dependencies = []`. The package imports the standard library only, and of it only what every
22
+ build has: `hashlib`, `hmac`, `binascii`, `struct`, `math.isqrt` and the like. It does not import
23
+ `zlib`, which is optional in CPython builds: deflate, Adler-32, PNG, BMP, the JPEG encoder and
24
+ the rasteriser are written here. SHA-256, HMAC, PBKDF2 and CRC-32 are the standard library's.
25
+ - No third-party packages anywhere, including the tests and the tools. Tests use `unittest` only;
26
+ they may cross-check against `zlib` and may use floating point to measure error.
27
+ - The build backend is setuptools; `build` is the only package needed to make a release. No
28
+ linter, formatter or type checker is required to work on the code, and none is configured.
29
+ - License: MIT (`LICENSE`); contributions are accepted under it.
30
+
31
+ ## Style
32
+
33
+ - Everything is English: code, comments, docstrings, documentation, commit messages. No emoji.
34
+ - PEP 8 with 100 columns, double quotes, type hints on every function, a docstring on every
35
+ public object. Public names live in `humanized_hash/__init__.py` and `__all__`; every other
36
+ module starts with an underscore.
37
+ - Python 3.9 is the oldest supported version: `from __future__ import annotations` in every
38
+ module, `typing.Optional` and `typing.Union` where an annotation is evaluated at run time, no
39
+ `match`, no `int.bit_count`, no `zip(strict=...)`.
40
+ - Comments explain the code and cite the section of the specification or the standard
41
+ (RFC 1951, ITU-T T.81).
42
+
43
+ ## Library rules (src/humanized_hash)
44
+
45
+ - Integer arithmetic only: no `float`, no `/`, no `round`, nothing from `math` but `isqrt`.
46
+ Python integers are unbounded; mask where the specification works modulo 2^32.
47
+ - Invalid values raise `HhError` (a `ValueError`) with the `ErrorCode` of the specification, in
48
+ the order of checks the specification gives; arguments of the wrong type raise `TypeError`.
49
+ - Buffers that held key material are overwritten before release, as far as Python allows; the
50
+ documentation says where it does not.
51
+ - A faster path must equal the per-sample definition exactly. `tests/reference.py` is that
52
+ definition; `tests/test_raster.py` compares the two.
53
+ - `tests/test_package.py` enforces the rules that a program can check.
54
+
55
+ ## Build and test
56
+
57
+ - `python -m unittest discover -s tests -t .` runs every test against the source tree; it must
58
+ pass with `python -W error -X dev` on every supported version. `HH_TEST_INSTALLED=1` tests the
59
+ installed package instead, `HH_TESTDATA_DIR` points at the vectors.
60
+ - `tools/crosscheck.sh <hh_cli>` runs the differential test against hh-cpp: generated cases and
61
+ the hand-made cases of `tools/edge-cases.txt`, a file of bytes that every implementation
62
+ carries in the same copy; `python tools/bench.py` times every step. The tools may use floating
63
+ point, the library never.
64
+ - `tools/update-vectors.sh <hh-cpp checkout>` refreshes `testdata/` and `testdata/SOURCE`; never
65
+ edit those files by hand.
66
+ - `python -m build` makes the source distribution and the wheel. The version lives in
67
+ `src/humanized_hash/_version.py` and nowhere else.
68
+
69
+ ## Commits
70
+
71
+ Atomic, imperative, lower case, for example "add the baseline jpeg encoder". Every commit passes
72
+ the tests.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dmitry Mandrika
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ # The source distribution carries everything needed to test it: the tests, the golden vectors of
2
+ # hh-cpp, the tools and the documentation. The wheel holds the package only.
3
+ include CHANGELOG.md CONTRIBUTING.md
4
+ graft docs
5
+ graft testdata
6
+ recursive-include tests *.py
7
+ recursive-include tools *.sh *.py *.txt
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: humanized-hash
3
+ Version: 1.0.0
4
+ Summary: Humanized Hash (hh): an address, a public key or a hash as a small deterministic picture that a person can compare at a glance
5
+ Author: Dmitry Mandrika
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/censync/hh-python
8
+ Project-URL: Documentation, https://github.com/censync/hh-python/blob/main/docs/INTEGRATION.md
9
+ Project-URL: Specification, https://github.com/censync/hh-cpp/blob/v1.0.0/docs/SPEC.md
10
+ Project-URL: Changelog, https://github.com/censync/hh-python/blob/main/CHANGELOG.md
11
+ Project-URL: Issues, https://github.com/censync/hh-python/issues
12
+ Keywords: address poisoning,blockchain,fingerprint,identicon,visual hash,wallet
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: Implementation :: CPython
25
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
26
+ Classifier: Topic :: Multimedia :: Graphics
27
+ Classifier: Topic :: Security :: Cryptography
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.9
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Dynamic: license-file
33
+
34
+ # hh - Humanized Hash (Python)
35
+
36
+ hh turns a blockchain address, a public key or any hash into a small deterministic picture that a
37
+ person can compare at a glance: a 4 x 4 matrix of solid squares, circles and triangles in four
38
+ colours. It exists to catch address poisoning and clipboard substitution, which work because
39
+ people check only the first and last characters of a long string. The colours are chosen so that
40
+ people with a colour vision deficiency can tell them apart as well.
41
+
42
+ | `0x1234567890abcdef00112233445566778899aabb` | `0x12345678f1e2d3c4b5a69788796a5b4c8899aabb` |
43
+ |---|---|
44
+ | ![picture of the first address](https://raw.githubusercontent.com/censync/hh-python/v1.0.0/testdata/golden/poison-a-universal-128.png) | ![picture of the second address](https://raw.githubusercontent.com/censync/hh-python/v1.0.0/testdata/golden/poison-b-universal-128.png) |
45
+
46
+ The two addresses agree in their first and last eight hex digits. Their pictures are unrelated.
47
+
48
+ This is the Python implementation, hh-python, published on PyPI as `humanized-hash`. It is pure
49
+ Python, depends on the standard library only, supports CPython 3.9 to 3.14 and PyPy 3.10, and
50
+ produces, byte for byte, the output of the C++ reference implementation
51
+ [hh-cpp](https://github.com/censync/hh-cpp), which owns the
52
+ [specification](https://github.com/censync/hh-cpp/blob/v1.0.0/docs/SPEC.md) and the golden vectors.
53
+ `testdata/` is a byte-identical copy of those vectors; `testdata/SOURCE` names the hh-cpp release
54
+ they came from. The same pictures come from [hh-kotlin](https://github.com/censync/hh-kotlin) (JVM
55
+ and Android, Maven `io.github.censync:hh`), [hh-ts](https://github.com/censync/hh-ts) (TypeScript,
56
+ npm `@censync/hh`) and [go-hh](https://github.com/censync/go-hh) (Go, `github.com/censync/go-hh`).
57
+
58
+ ## A longer example: Sui
59
+
60
+ A Sui address has 64 hex digits, and nobody reads 64 digits. The second address below differs from
61
+ the first in one digit, the third in two; the changed digits are marked. In the text they are easy
62
+ to miss. The pictures and the tags are unrelated, because every cell depends on every bit of the
63
+ input.
64
+
65
+ | Picture | Address | Tag |
66
+ |---|---|---|
67
+ | ![picture of the first Sui address](https://raw.githubusercontent.com/censync/hh-python/v1.0.0/docs/images/sui-a.png) | <code>0xeab3150efcb34ff74930d8f3d491be109070a39e4d380de7737aff5c72a0b6b2</code> | `B6P-65H` |
68
+ | ![picture of the second Sui address](https://raw.githubusercontent.com/censync/hh-python/v1.0.0/docs/images/sui-b.png) | <code>0xeab3150efcb34ff74930d8f<ins><b>8</b></ins>d491be109070a39e4d380de7737aff5c72a0b6b2</code> | `Q60-QKR` |
69
+ | ![picture of the third Sui address](https://raw.githubusercontent.com/censync/hh-python/v1.0.0/docs/images/sui-c.png) | <code>0xeab3150efcb34ff74930d8f3d491be1090<ins><b>1</b></ins>0a39e4d380d<ins><b>c</b></ins>7737aff5c72a0b6b2</code> | `ZSJ-7BK` |
70
+
71
+ What a forger pays, by calculation. One current GPU tries about 1.4 billion addresses per second;
72
+ a try against hh also has to compute the stretched base digest, which leaves about 680 000 tries
73
+ per second. The figures are the expected search times on one such GPU for a typical picture
74
+ ([SECURITY.md](https://github.com/censync/hh-cpp/blob/v1.0.0/docs/SECURITY.md) of hh-cpp has the
75
+ reasoning).
76
+
77
+ | The forged address has to match | Tries | One GPU |
78
+ |---|---|---|
79
+ | the first 4 and the last 4 hex digits | 2^32 | 3 seconds |
80
+ | the first 6 and the last 6 hex digits | 2^48 | 2.3 days |
81
+ | the first 8 and the last 8 hex digits | 2^64 | 420 years |
82
+ | the universal picture, with two cells allowed to differ | 2^52, stretched | 210 years |
83
+ | the universal picture, in every cell | 2^68, stretched | 14 million years |
84
+ | the ends of the text and the picture | the product of the two | |
85
+ | the keyed picture | cannot be searched: without the key the picture cannot be computed | |
86
+
87
+ A lookalike of the text is cheap, which is why address poisoning works. A lookalike of the
88
+ picture is not, and the two costs multiply. A picture that looks the same is still strong
89
+ evidence rather than proof; the tag or the full address is the check that is certain.
90
+
91
+ ## Properties
92
+
93
+ - **Two modes.** A *universal* picture is the same for everyone and is what two people compare. A
94
+ *keyed* picture is computed with a 32-byte secret of the wallet: an attacker who does not hold
95
+ the key cannot compute, and therefore cannot grind, a lookalike. Inside an application keyed
96
+ pictures are the default.
97
+ - **Deterministic to the byte.** Integer arithmetic only. The same input gives the same pixels
98
+ and the same PNG, BMP and JPEG bytes as hh-cpp, on every platform and every interpreter.
99
+ - **Frozen.** The algorithm has no version and never changes; a picture that a user has learned
100
+ stays the same for ever. Library releases follow SemVer and never alter the output.
101
+ - **No dependencies.** `dependencies = []`: no Pillow, no NumPy, not even `zlib`. SHA-256, HMAC
102
+ and PBKDF2 come from `hashlib` and `hmac`; deflate, PNG, BMP, the baseline JPEG encoder and the
103
+ rasteriser are part of the library.
104
+ - **Fast enough for an interpreter.** The rasteriser works on whole rows of samples with integer
105
+ bit sets instead of looping over samples: on a desktop core a 128-pixel picture takes one to
106
+ two milliseconds, its PNG three to four, the base digest two to four.
107
+ - **Made for colour vision deficiency.** About one man in twelve does not see colours the way the
108
+ rest do. The four colours were chosen for them: the palette was searched so that every pair stays
109
+ apart under simulated protanopia, deuteranopia and tritanopia, and every colour keeps a contrast
110
+ of 3:1 on white and on dark surfaces. Shape carries most of the information, so a picture still
111
+ works in greyscale (the measurements are in
112
+ [docs/design](https://github.com/censync/hh-cpp/tree/v1.0.0/docs/design) of hh-cpp).
113
+ - **Pixels, not pictures.** The library returns RGBA bytes and encoded files; making a Tkinter,
114
+ Qt or Pillow image of them is one line in the host.
115
+ - **Typed.** Type hints throughout and a `py.typed` marker.
116
+
117
+ ## Quick start
118
+
119
+ ```sh
120
+ pip install humanized-hash
121
+ ```
122
+
123
+ ```python
124
+ from humanized_hash import BaseDigest, Fingerprint
125
+
126
+ digest = BaseDigest.of_hex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") # slow: cache it
127
+ fingerprint = Fingerprint.universal(digest) # or Fingerprint.keyed(digest, key)
128
+ image = fingerprint.render(128) # 128 x 128 RGBA pixels, image.rgba
129
+
130
+ png: bytes = image.encode_png() # or image.save("address.png")
131
+ tag: str = fingerprint.tag # "TKSPVH", shown as TKS-PVH
132
+ ```
133
+
134
+ A keyed picture needs the 32-byte secret of the wallet:
135
+
136
+ ```python
137
+ from humanized_hash import SecretKey
138
+
139
+ with SecretKey(key_bytes) as key: # wiped when the block ends
140
+ private = Fingerprint.keyed(digest, key)
141
+ ```
142
+
143
+ `BaseDigest.of` takes the bytes of an address, `of_hex` their hexadecimal spelling and `of_text` an
144
+ address that exists only as text. `BaseDigest.from_bytes` computes nothing: it restores a digest
145
+ that was cached as `bytes(digest)`.
146
+
147
+ Invalid values raise `HhError`, a `ValueError` whose `code` is the error of the specification;
148
+ arguments of the wrong type raise `TypeError`.
149
+
150
+ A decision (confirming a payment, verifying a pasted address) should be backed by a picture of at
151
+ least 64 device-independent pixels, better 96, next to the picture it is compared with. Smaller
152
+ pictures are for recognition in lists. See
153
+ [docs/INTEGRATION.md](https://github.com/censync/hh-python/blob/v1.0.0/docs/INTEGRATION.md) for
154
+ web backends, Tkinter, Pillow and Qt, caching and key handling, and
155
+ [SECURITY.md](https://github.com/censync/hh-cpp/blob/v1.0.0/docs/SECURITY.md) of hh-cpp for what
156
+ a picture proves and what it does not.
157
+
158
+ ## Command line
159
+
160
+ The package installs the command `humanized-hash`; `python -m humanized_hash` is the same tool.
161
+
162
+ ```sh
163
+ humanized-hash 0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed --out address.png
164
+ humanized-hash --text bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 --size 256 --out address.png
165
+ humanized-hash <hex> --key <64 hex digits> --shape round --frame double --out private.png
166
+ ```
167
+
168
+ It takes the options of `hh_cli` of hh-cpp and prints the same values. It is a demonstration and
169
+ a test tool: a real host never takes a key from the command line.
170
+
171
+ ## Testing
172
+
173
+ Python 3.9 or newer; nothing to install.
174
+
175
+ ```sh
176
+ python -m unittest discover -s tests -t . # every test, against the source tree
177
+ tools/crosscheck.sh <path to hh_cli of hh-cpp> # differential test against hh-cpp
178
+ python tools/bench.py # what each step costs on this machine
179
+ python -m build # sdist and wheel (needs the build package)
180
+ ```
181
+
182
+ The tests reproduce every record of the golden vectors, compare the rasteriser with a
183
+ per-sample transcription of the specification and decode every encoder's output. The rules for
184
+ patches are in
185
+ [CONTRIBUTING.md](https://github.com/censync/hh-python/blob/v1.0.0/CONTRIBUTING.md), the releases
186
+ in [CHANGELOG.md](https://github.com/censync/hh-python/blob/v1.0.0/CHANGELOG.md).
187
+
188
+ ## License
189
+
190
+ MIT, see [LICENSE](https://github.com/censync/hh-python/blob/v1.0.0/LICENSE).
191
+ Copyright (c) 2026 Dmitry Mandrika. [CenSync](https://censync.com)
@@ -0,0 +1,158 @@
1
+ # hh - Humanized Hash (Python)
2
+
3
+ hh turns a blockchain address, a public key or any hash into a small deterministic picture that a
4
+ person can compare at a glance: a 4 x 4 matrix of solid squares, circles and triangles in four
5
+ colours. It exists to catch address poisoning and clipboard substitution, which work because
6
+ people check only the first and last characters of a long string. The colours are chosen so that
7
+ people with a colour vision deficiency can tell them apart as well.
8
+
9
+ | `0x1234567890abcdef00112233445566778899aabb` | `0x12345678f1e2d3c4b5a69788796a5b4c8899aabb` |
10
+ |---|---|
11
+ | ![picture of the first address](https://raw.githubusercontent.com/censync/hh-python/v1.0.0/testdata/golden/poison-a-universal-128.png) | ![picture of the second address](https://raw.githubusercontent.com/censync/hh-python/v1.0.0/testdata/golden/poison-b-universal-128.png) |
12
+
13
+ The two addresses agree in their first and last eight hex digits. Their pictures are unrelated.
14
+
15
+ This is the Python implementation, hh-python, published on PyPI as `humanized-hash`. It is pure
16
+ Python, depends on the standard library only, supports CPython 3.9 to 3.14 and PyPy 3.10, and
17
+ produces, byte for byte, the output of the C++ reference implementation
18
+ [hh-cpp](https://github.com/censync/hh-cpp), which owns the
19
+ [specification](https://github.com/censync/hh-cpp/blob/v1.0.0/docs/SPEC.md) and the golden vectors.
20
+ `testdata/` is a byte-identical copy of those vectors; `testdata/SOURCE` names the hh-cpp release
21
+ they came from. The same pictures come from [hh-kotlin](https://github.com/censync/hh-kotlin) (JVM
22
+ and Android, Maven `io.github.censync:hh`), [hh-ts](https://github.com/censync/hh-ts) (TypeScript,
23
+ npm `@censync/hh`) and [go-hh](https://github.com/censync/go-hh) (Go, `github.com/censync/go-hh`).
24
+
25
+ ## A longer example: Sui
26
+
27
+ A Sui address has 64 hex digits, and nobody reads 64 digits. The second address below differs from
28
+ the first in one digit, the third in two; the changed digits are marked. In the text they are easy
29
+ to miss. The pictures and the tags are unrelated, because every cell depends on every bit of the
30
+ input.
31
+
32
+ | Picture | Address | Tag |
33
+ |---|---|---|
34
+ | ![picture of the first Sui address](https://raw.githubusercontent.com/censync/hh-python/v1.0.0/docs/images/sui-a.png) | <code>0xeab3150efcb34ff74930d8f3d491be109070a39e4d380de7737aff5c72a0b6b2</code> | `B6P-65H` |
35
+ | ![picture of the second Sui address](https://raw.githubusercontent.com/censync/hh-python/v1.0.0/docs/images/sui-b.png) | <code>0xeab3150efcb34ff74930d8f<ins><b>8</b></ins>d491be109070a39e4d380de7737aff5c72a0b6b2</code> | `Q60-QKR` |
36
+ | ![picture of the third Sui address](https://raw.githubusercontent.com/censync/hh-python/v1.0.0/docs/images/sui-c.png) | <code>0xeab3150efcb34ff74930d8f3d491be1090<ins><b>1</b></ins>0a39e4d380d<ins><b>c</b></ins>7737aff5c72a0b6b2</code> | `ZSJ-7BK` |
37
+
38
+ What a forger pays, by calculation. One current GPU tries about 1.4 billion addresses per second;
39
+ a try against hh also has to compute the stretched base digest, which leaves about 680 000 tries
40
+ per second. The figures are the expected search times on one such GPU for a typical picture
41
+ ([SECURITY.md](https://github.com/censync/hh-cpp/blob/v1.0.0/docs/SECURITY.md) of hh-cpp has the
42
+ reasoning).
43
+
44
+ | The forged address has to match | Tries | One GPU |
45
+ |---|---|---|
46
+ | the first 4 and the last 4 hex digits | 2^32 | 3 seconds |
47
+ | the first 6 and the last 6 hex digits | 2^48 | 2.3 days |
48
+ | the first 8 and the last 8 hex digits | 2^64 | 420 years |
49
+ | the universal picture, with two cells allowed to differ | 2^52, stretched | 210 years |
50
+ | the universal picture, in every cell | 2^68, stretched | 14 million years |
51
+ | the ends of the text and the picture | the product of the two | |
52
+ | the keyed picture | cannot be searched: without the key the picture cannot be computed | |
53
+
54
+ A lookalike of the text is cheap, which is why address poisoning works. A lookalike of the
55
+ picture is not, and the two costs multiply. A picture that looks the same is still strong
56
+ evidence rather than proof; the tag or the full address is the check that is certain.
57
+
58
+ ## Properties
59
+
60
+ - **Two modes.** A *universal* picture is the same for everyone and is what two people compare. A
61
+ *keyed* picture is computed with a 32-byte secret of the wallet: an attacker who does not hold
62
+ the key cannot compute, and therefore cannot grind, a lookalike. Inside an application keyed
63
+ pictures are the default.
64
+ - **Deterministic to the byte.** Integer arithmetic only. The same input gives the same pixels
65
+ and the same PNG, BMP and JPEG bytes as hh-cpp, on every platform and every interpreter.
66
+ - **Frozen.** The algorithm has no version and never changes; a picture that a user has learned
67
+ stays the same for ever. Library releases follow SemVer and never alter the output.
68
+ - **No dependencies.** `dependencies = []`: no Pillow, no NumPy, not even `zlib`. SHA-256, HMAC
69
+ and PBKDF2 come from `hashlib` and `hmac`; deflate, PNG, BMP, the baseline JPEG encoder and the
70
+ rasteriser are part of the library.
71
+ - **Fast enough for an interpreter.** The rasteriser works on whole rows of samples with integer
72
+ bit sets instead of looping over samples: on a desktop core a 128-pixel picture takes one to
73
+ two milliseconds, its PNG three to four, the base digest two to four.
74
+ - **Made for colour vision deficiency.** About one man in twelve does not see colours the way the
75
+ rest do. The four colours were chosen for them: the palette was searched so that every pair stays
76
+ apart under simulated protanopia, deuteranopia and tritanopia, and every colour keeps a contrast
77
+ of 3:1 on white and on dark surfaces. Shape carries most of the information, so a picture still
78
+ works in greyscale (the measurements are in
79
+ [docs/design](https://github.com/censync/hh-cpp/tree/v1.0.0/docs/design) of hh-cpp).
80
+ - **Pixels, not pictures.** The library returns RGBA bytes and encoded files; making a Tkinter,
81
+ Qt or Pillow image of them is one line in the host.
82
+ - **Typed.** Type hints throughout and a `py.typed` marker.
83
+
84
+ ## Quick start
85
+
86
+ ```sh
87
+ pip install humanized-hash
88
+ ```
89
+
90
+ ```python
91
+ from humanized_hash import BaseDigest, Fingerprint
92
+
93
+ digest = BaseDigest.of_hex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") # slow: cache it
94
+ fingerprint = Fingerprint.universal(digest) # or Fingerprint.keyed(digest, key)
95
+ image = fingerprint.render(128) # 128 x 128 RGBA pixels, image.rgba
96
+
97
+ png: bytes = image.encode_png() # or image.save("address.png")
98
+ tag: str = fingerprint.tag # "TKSPVH", shown as TKS-PVH
99
+ ```
100
+
101
+ A keyed picture needs the 32-byte secret of the wallet:
102
+
103
+ ```python
104
+ from humanized_hash import SecretKey
105
+
106
+ with SecretKey(key_bytes) as key: # wiped when the block ends
107
+ private = Fingerprint.keyed(digest, key)
108
+ ```
109
+
110
+ `BaseDigest.of` takes the bytes of an address, `of_hex` their hexadecimal spelling and `of_text` an
111
+ address that exists only as text. `BaseDigest.from_bytes` computes nothing: it restores a digest
112
+ that was cached as `bytes(digest)`.
113
+
114
+ Invalid values raise `HhError`, a `ValueError` whose `code` is the error of the specification;
115
+ arguments of the wrong type raise `TypeError`.
116
+
117
+ A decision (confirming a payment, verifying a pasted address) should be backed by a picture of at
118
+ least 64 device-independent pixels, better 96, next to the picture it is compared with. Smaller
119
+ pictures are for recognition in lists. See
120
+ [docs/INTEGRATION.md](https://github.com/censync/hh-python/blob/v1.0.0/docs/INTEGRATION.md) for
121
+ web backends, Tkinter, Pillow and Qt, caching and key handling, and
122
+ [SECURITY.md](https://github.com/censync/hh-cpp/blob/v1.0.0/docs/SECURITY.md) of hh-cpp for what
123
+ a picture proves and what it does not.
124
+
125
+ ## Command line
126
+
127
+ The package installs the command `humanized-hash`; `python -m humanized_hash` is the same tool.
128
+
129
+ ```sh
130
+ humanized-hash 0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed --out address.png
131
+ humanized-hash --text bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 --size 256 --out address.png
132
+ humanized-hash <hex> --key <64 hex digits> --shape round --frame double --out private.png
133
+ ```
134
+
135
+ It takes the options of `hh_cli` of hh-cpp and prints the same values. It is a demonstration and
136
+ a test tool: a real host never takes a key from the command line.
137
+
138
+ ## Testing
139
+
140
+ Python 3.9 or newer; nothing to install.
141
+
142
+ ```sh
143
+ python -m unittest discover -s tests -t . # every test, against the source tree
144
+ tools/crosscheck.sh <path to hh_cli of hh-cpp> # differential test against hh-cpp
145
+ python tools/bench.py # what each step costs on this machine
146
+ python -m build # sdist and wheel (needs the build package)
147
+ ```
148
+
149
+ The tests reproduce every record of the golden vectors, compare the rasteriser with a
150
+ per-sample transcription of the specification and decode every encoder's output. The rules for
151
+ patches are in
152
+ [CONTRIBUTING.md](https://github.com/censync/hh-python/blob/v1.0.0/CONTRIBUTING.md), the releases
153
+ in [CHANGELOG.md](https://github.com/censync/hh-python/blob/v1.0.0/CHANGELOG.md).
154
+
155
+ ## License
156
+
157
+ MIT, see [LICENSE](https://github.com/censync/hh-python/blob/v1.0.0/LICENSE).
158
+ Copyright (c) 2026 Dmitry Mandrika. [CenSync](https://censync.com)