The visual trap
Two pieces of text can look alike while containing different Unicode code points. That distinction matters when software turns text into bytes. This experiment uses the ordinary word café in two representations; it contains no password or seed.
Run the experiment
This is an original KeychainX exercise using invented data. It runs locally with Python 3 and makes no network requests. Download the script, read it, and run it in a folder used for practice.
python unicode_bytes.py"""Synthetic Unicode exercise. Never enter a real recovery password."""
import unicodedata
composed = "caf\u00e9"
decomposed = "cafe\u0301"
print("Equal strings:", composed == decomposed)
print("UTF-8 A:", composed.encode("utf-8").hex())
print("UTF-8 B:", decomposed.encode("utf-8").hex())
print("NFC equal:", unicodedata.normalize("NFC", composed) == unicodedata.normalize("NFC", decomposed))
assert composed != decomposed
assert unicodedata.normalize("NFC", composed) == unicodedata.normalize("NFC", decomposed)
Expected observations
The raw strings compare unequal. Their UTF-8 hex values are 636166c3a9 and 63616665cc81. Normalizing both examples to NFC makes them compare equal. Python’s unicodedata module exposes normalization for this controlled comparison.
Keep interpretation separate from alteration
This result does not mean every wallet normalizes text to NFC. Some protocols specify a different normalization form, and an application may preserve input exactly. A blanket “clean up the password” operation can destroy evidence about what was originally entered. Keep the original transcription unchanged and document proposed transformations separately.
Extend the experiment
- Compare an ordinary space with a non-breaking space in invented text.
- Add a trailing space and inspect the length and bytes.
- Record the keyboard layout and operating system when investigating real input history, without publishing the input.
- Confirm the actual wallet’s documented encoding before applying any hypothesis to an authorized recovery.