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.
- humanized_hash-1.0.0/CHANGELOG.md +46 -0
- humanized_hash-1.0.0/CONTRIBUTING.md +72 -0
- humanized_hash-1.0.0/LICENSE +21 -0
- humanized_hash-1.0.0/MANIFEST.in +7 -0
- humanized_hash-1.0.0/PKG-INFO +191 -0
- humanized_hash-1.0.0/README.md +158 -0
- humanized_hash-1.0.0/docs/INTEGRATION.md +221 -0
- humanized_hash-1.0.0/docs/images/sui-a.png +0 -0
- humanized_hash-1.0.0/docs/images/sui-b.png +0 -0
- humanized_hash-1.0.0/docs/images/sui-c.png +0 -0
- humanized_hash-1.0.0/pyproject.toml +61 -0
- humanized_hash-1.0.0/setup.cfg +4 -0
- humanized_hash-1.0.0/src/humanized_hash/__init__.py +56 -0
- humanized_hash-1.0.0/src/humanized_hash/__main__.py +10 -0
- humanized_hash-1.0.0/src/humanized_hash/_api.py +388 -0
- humanized_hash-1.0.0/src/humanized_hash/_bmp.py +38 -0
- humanized_hash-1.0.0/src/humanized_hash/_cli.py +514 -0
- humanized_hash-1.0.0/src/humanized_hash/_contrast.py +71 -0
- humanized_hash-1.0.0/src/humanized_hash/_derive.py +145 -0
- humanized_hash-1.0.0/src/humanized_hash/_errors.py +63 -0
- humanized_hash-1.0.0/src/humanized_hash/_flatten.py +31 -0
- humanized_hash-1.0.0/src/humanized_hash/_image.py +162 -0
- humanized_hash-1.0.0/src/humanized_hash/_jpeg.py +360 -0
- humanized_hash-1.0.0/src/humanized_hash/_model.py +185 -0
- humanized_hash-1.0.0/src/humanized_hash/_png.py +184 -0
- humanized_hash-1.0.0/src/humanized_hash/_raster.py +396 -0
- humanized_hash-1.0.0/src/humanized_hash/_version.py +5 -0
- humanized_hash-1.0.0/src/humanized_hash/py.typed +0 -0
- humanized_hash-1.0.0/src/humanized_hash.egg-info/PKG-INFO +191 -0
- humanized_hash-1.0.0/src/humanized_hash.egg-info/SOURCES.txt +70 -0
- humanized_hash-1.0.0/src/humanized_hash.egg-info/dependency_links.txt +1 -0
- humanized_hash-1.0.0/src/humanized_hash.egg-info/entry_points.txt +2 -0
- humanized_hash-1.0.0/src/humanized_hash.egg-info/top_level.txt +1 -0
- humanized_hash-1.0.0/testdata/SOURCE +26 -0
- humanized_hash-1.0.0/testdata/golden/evm-1-keyed-128-dark.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-1-keyed-128.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-1-keyed-48.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-1-universal-128.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-1-universal-256-transparent.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-1-universal-32.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-brackets.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-chamfered.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-double.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-round-double.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-round-gaps.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-round-thick.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-round-ticks.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-2-keyed-128-thick.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-2-universal-128-plain.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-2-universal-128-round-plain.png +0 -0
- humanized_hash-1.0.0/testdata/golden/evm-2-universal-128-round.png +0 -0
- humanized_hash-1.0.0/testdata/golden/poison-a-universal-128.png +0 -0
- humanized_hash-1.0.0/testdata/golden/poison-b-universal-128.png +0 -0
- humanized_hash-1.0.0/testdata/golden/sampler-universal-128.png +0 -0
- humanized_hash-1.0.0/testdata/vectors.tsv +271 -0
- humanized_hash-1.0.0/tests/__init__.py +13 -0
- humanized_hash-1.0.0/tests/decoders.py +225 -0
- humanized_hash-1.0.0/tests/reference.py +209 -0
- humanized_hash-1.0.0/tests/support.py +67 -0
- humanized_hash-1.0.0/tests/test_api.py +706 -0
- humanized_hash-1.0.0/tests/test_cli.py +456 -0
- humanized_hash-1.0.0/tests/test_decoders.py +241 -0
- humanized_hash-1.0.0/tests/test_docs.py +87 -0
- humanized_hash-1.0.0/tests/test_package.py +134 -0
- humanized_hash-1.0.0/tests/test_primitives.py +167 -0
- humanized_hash-1.0.0/tests/test_raster.py +125 -0
- humanized_hash-1.0.0/tests/test_robustness.py +145 -0
- humanized_hash-1.0.0/tests/test_vectors.py +177 -0
- humanized_hash-1.0.0/tools/bench.py +78 -0
- humanized_hash-1.0.0/tools/crosscheck.sh +48 -0
- humanized_hash-1.0.0/tools/edge-cases.txt +166 -0
- 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
|
+
|  |  |
|
|
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
|
+
|  | <code>0xeab3150efcb34ff74930d8f3d491be109070a39e4d380de7737aff5c72a0b6b2</code> | `B6P-65H` |
|
|
68
|
+
|  | <code>0xeab3150efcb34ff74930d8f<ins><b>8</b></ins>d491be109070a39e4d380de7737aff5c72a0b6b2</code> | `Q60-QKR` |
|
|
69
|
+
|  | <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
|
+
|  |  |
|
|
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
|
+
|  | <code>0xeab3150efcb34ff74930d8f3d491be109070a39e4d380de7737aff5c72a0b6b2</code> | `B6P-65H` |
|
|
35
|
+
|  | <code>0xeab3150efcb34ff74930d8f<ins><b>8</b></ins>d491be109070a39e4d380de7737aff5c72a0b6b2</code> | `Q60-QKR` |
|
|
36
|
+
|  | <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)
|