Irreversible Transformations: Hash Functions and Iron Gall Ink
Iron gall ink forms when you combine tannin from oak galls or walnut hulls with iron sulfate. The ferrous ions bind to the tannic acid, oxidize to ferric, and create a blue-black precipitate that’s been stable in medieval manuscripts for centuries. The reaction is irreversible—once the iron and tannin combine, you can’t separate them back into their original components. You’ve transformed the inputs into something fundamentally different.
Hash functions work the same way. You feed in data—arbitrary bytes, a password, a file—and get back a fixed-size digest. The transformation is deterministic (same input always produces the same hash) but one-way. You cannot reverse-engineer the original input from the hash output. Small changes in the input cascade through the entire digest.
recipe = "5g walnut hulls, 2g iron sulfate, 1g gum arabic"
ink = :crypto.hash(:sha256, recipe) |> Base.encode16(case: :lower)
IO.puts("Recipe: #{recipe}")
IO.puts("Ink: #{String.slice(ink, 0, 32)}...")
# Change one character
recipe2 = "5g walnut hulls, 3g iron sulfate, 1g gum arabic"
ink2 = :crypto.hash(:sha256, recipe2) |> Base.encode16(case: :lower)
IO.puts("\nRecipe: #{recipe2}")
IO.puts("Ink: #{String.slice(ink2, 0, 32)}...")
In the shell, you’d hash a recipe file the same way you’d verify a downloaded ISO hasn’t been tampered with:
echo "walnut hulls + iron sulfate + gum arabic" | sha256sum | cut -c1-32
The BEAM’s :crypto module wraps OpenSSL, giving you MD5, SHA-1, SHA-256, and the newer SHA-3 family. Bash relies on GNU coreutils or system binaries. Both produce the same digest for the same input, because the algorithm is standardized.
The parallel breaks down in one way: ink-making chemistry is constrained by thermodynamics and reaction kinetics. Hash functions are constrained by mathematics and chosen design. But both share the same directionality—once you’ve committed the input to the transformation, you’re working with the product, not the ingredients.