hh

package module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 25, 2026 License: MIT Imports: 14 Imported by: 0

README

hh - Humanized Hash (Go)

hh turns a blockchain address, a public key or any hash into a small deterministic picture that a person can compare at a glance: a 4 x 4 matrix of solid squares, circles and triangles in four colours. It exists to catch address poisoning and clipboard substitution, which work because people check only the first and last characters of a long string. The colours are chosen so that people with a colour vision deficiency can tell them apart as well.

0x1234567890abcdef00112233445566778899aabb 0x12345678f1e2d3c4b5a69788796a5b4c8899aabb
picture of the first address picture of the second address

The two addresses agree in their first and last eight hex digits. Their pictures are unrelated.

This is the Go implementation. It uses the Go standard library only and produces, byte for byte, the output of the C++ reference implementation hh-cpp, which owns the specification and the golden vectors. testdata/ is a byte-identical copy of those vectors; testdata/SOURCE names the hh-cpp release they came from.

Implementations

Every implementation produces the same pictures, tags and encoded files, byte for byte, and its tests check it against a copy of the golden vectors of hh-cpp.

Language Repository Package Install
C++17, C ABI hh-cpp, the reference: specification and golden vectors CMake hh::hh, pkg-config hh (releases) CMake FetchContent or find_package(hh)
Kotlin and Java: JVM, Android hh-kotlin Maven Central io.github.censync:hh implementation("io.github.censync:hh:1.1.0")
TypeScript and JavaScript: browsers, Node.js, Deno, Bun hh-ts npm @censync/hh npm install @censync/hh
Go go-hh (this repository) github.com/censync/go-hh go get github.com/censync/go-hh
Python hh-python PyPI humanized-hash pip install humanized-hash

A longer example: Sui

A Sui address has 64 hex digits, and nobody reads 64 digits. The second address below differs from the first in one digit, the third in two; the changed digits are marked. In the text they are easy to miss. The pictures and the tags are unrelated, because every cell depends on every bit of the input.

Picture Address Tag
picture of the first Sui address 0xeab3150efcb34ff74930d8f3d491be109070a39e4d380de7737aff5c72a0b6b2 B6P-65H
picture of the second Sui address 0xeab3150efcb34ff74930d8f8d491be109070a39e4d380de7737aff5c72a0b6b2 Q60-QKR
picture of the third Sui address 0xeab3150efcb34ff74930d8f3d491be109010a39e4d380dc7737aff5c72a0b6b2 ZSJ-7BK

What a forger pays, by calculation. One current GPU tries about 1.4 billion addresses per second; a try against hh also has to compute the stretched base digest, which leaves about 680 000 tries per second. The figures are the expected search times on one such GPU for a typical picture (SECURITY.md of hh-cpp has the reasoning).

The forged address has to match Tries One GPU
the first 4 and the last 4 hex digits 2^32 3 seconds
the first 6 and the last 6 hex digits 2^48 2.3 days
the first 8 and the last 8 hex digits 2^64 420 years
the universal picture, with two cells allowed to differ 2^52, stretched 210 years
the universal picture, in every cell 2^68, stretched 14 million years
the ends of the text and the picture the product of the two
the keyed picture cannot be searched: without the key the picture cannot be computed

A lookalike of the text is cheap, which is why address poisoning works. A lookalike of the picture is not, and the two costs multiply. A picture that looks the same is still strong evidence rather than proof; the tag or the full address is the check that is certain.

Properties

  • Two modes. A universal picture is the same for everyone and is what two people compare. A keyed picture is computed with a 32-byte secret of the wallet: an attacker who does not hold the key cannot compute, and therefore cannot grind, a lookalike. Inside an application keyed pictures are the default.
  • Deterministic to the byte. Integer arithmetic only. The same input gives the same pixels and the same PNG, BMP and JPEG bytes as hh-cpp, on every platform.
  • Frozen. The algorithm has no version and never changes; a picture that a user has learned stays the same for ever. Library releases follow SemVer and never alter the output.
  • No dependencies. go.mod has no require line. SHA-256, HMAC, CRC-32 and Adler-32 come from the standard library; PBKDF2, the deflate stream and the PNG, BMP and JPEG encoders, whose output the specification fixes byte for byte, are part of the package. It does not import image/png, image/jpeg, compress/... or math.
  • Made for colour vision deficiency. About one man in twelve does not see colours the way the rest do. The four colours were chosen for them: the palette was searched so that every pair stays apart under simulated protanopia, deuteranopia and tritanopia, and every colour keeps a contrast of 3:1 on white and on dark surfaces. Shape carries most of the information, so a picture still works in greyscale (the measurements are in docs/design of hh-cpp).
  • Pixels, not pictures. The package returns straight-alpha RGBA pixels and encoded files; Image.NRGBA hands the pixels to the standard image packages without a copy.

Where it is used

hh answers one question: is this the same address as the one I mean? Wherever a person has to answer it from a long string, a picture answers it faster and more reliably than the first and last characters do.

  • Sending and confirming. The picture of the recipient stands next to the address field and on the confirmation screen. A swapped or mistyped address changes it completely.
  • Address books and account lists. Every saved payee and every account of the user carries its picture, so a list is scanned instead of read; 32 to 48 px is enough for recognition.
  • Two devices of one user. An offline signer and the online device show the same picture for the same address, so the two screens are compared at a glance instead of 64 characters.
  • Support, screenshots and voice. The six-character tag (TKS-PVH) travels through chat and over the phone; the picture travels in a screenshot.
  • Documents and messages from a server. The encoders return PNG, BMP or JPEG bytes, so a backend puts the picture into a receipt, an invoice or an email without a graphics library.
  • Anything that is a hash, not only an address. An SSH or PGP key fingerprint, a TLS certificate pin, an API key, the checksum of a backup or of a firmware image.

Three rules keep it honest: the picture complements the text check and never replaces it; inside one application keyed pictures are the default and universal pictures are what is shared with others; a picture that backs a decision is at least 64 dp and stands beside the picture it is compared with. docs/INTEGRATION.md has the rest.

Quick start

go get github.com/censync/go-hh@v1.1.0
import hh "github.com/censync/go-hh"

digest, err := hh.BaseDigestFromHex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") // slow: cache it
if err != nil {
    return err
}
fp := hh.Universal(digest)                          // or hh.Keyed(digest, key)
img, err := hh.Render(fp, 128, hh.RenderOptions{})  // 128 x 128 pixels
if err != nil {
    return err
}
png, err := img.EncodePNG()                         // or img.NRGBA() for the image packages
tag := fp.Tag()                                     // "TKSPVH", shown as TKS-PVH

Nothing panics: every function returns a result or one of the package's Err... values, which work with errors.Is and carry the numeric code of the specification (errors.As with *hh.Error). The package keeps no global state and is safe for concurrent use. It needs Go 1.21 or newer. RenderOptions and its field types read and write the names of the specification (ParseFrame, String, encoding.TextMarshaler), so a look can live in a JSON configuration file. The API is documented on pkg.go.dev, with runnable examples.

A decision (confirming a payment, verifying a pasted address) should be backed by a picture of at least 64 points, better 96, next to the picture it is compared with. Smaller pictures are for recognition in lists. See docs/INTEGRATION.md for net/http, image/draw and key handling recipes and for the product rules, and SECURITY.md of hh-cpp for what a picture proves and what it does not.

Looks

The cells, the palette and the geometry are fixed; the host chooses the shape, the background and the frame, in either mode. Every picture below is the address of the quick start, rendered at 128 px.

Shape Opaque white Light blue E8EEF7 Transparent Keyed, transparent
Square square on white square on light blue square on a transparent background keyed square with rounded corners
Round round on white round on light blue round on a transparent background keyed round with ticks
  • Background. Any colour with any transparency. Outside rounded corners and outside the disc the picture is transparent anyway, so a transparent background takes whatever is behind it: the two transparent columns above are the same bytes on a light page and on a dark one.
  • Contrast. An opaque background is refused below 2:1 against a palette colour, and the contrast report gives the WCAG ratio so that a host can warn below 3:1. White scores 300, the light blue above 257, 121212 scores 300; mid greys and saturated surfaces are what to avoid.
  • Frames are open to both modes. Every style that fits the shape works for universal and keyed pictures alike. By default a universal picture has no frame and a keyed square gets rounded corners; a host that marks its keyed pictures picks one style and keeps it everywhere, and names the mode in the caption, since a frame alone proves nothing.
  • The round shape inscribes the same grid in a circle, so its cells are about a third smaller; give it a third more pixels.
options := hh.RenderOptions{
	Shape:      hh.ShapeRound,
	Background: hh.Transparent(), // or hh.Opaque(hh.RGB{0xE8, 0xEE, 0xF7})
	Frame:      hh.FrameTicks,    // any style of the shape, in either mode
}

report := hh.MeasureContrast(options, hh.White)
if report.FiguresX100 < 300 {
	// warn
}

A complete program

A command line program that writes the picture of an address to a PNG file and prints its tag.

mkdir hh-example && cd hh-example
go mod init example.com/hh-example
go get github.com/censync/go-hh@v1.1.0

main.go:

package main

import (
	"fmt"
	"log"
	"os"

	hh "github.com/censync/go-hh"
)

func main() {
	digest, err := hh.BaseDigestFromHex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
	if err != nil {
		log.Fatal(err)
	}
	fp := hh.Universal(digest)
	img, err := hh.Render(fp, 128, hh.RenderOptions{})
	if err != nil {
		log.Fatal(err)
	}
	png, err := img.EncodePNG()
	if err != nil {
		log.Fatal(err)
	}
	if err := os.WriteFile("address.png", png, 0o644); err != nil {
		log.Fatal(err)
	}
	tag := fp.Tag()
	fmt.Println(tag[:3] + "-" + tag[3:])
}

go run . prints TKS-PVH and writes address.png, byte for byte the file testdata/golden/evm-1-universal-128.png that every implementation reproduces.

Building

Go 1.21 or newer; nothing else.

go test ./...                                    # every test, the golden vectors included
go test -race ./...                              # the same under the race detector
go test -run '^$' -bench . .                     # base digest, render and encoder timings
go test -run '^$' -fuzz FuzzRender -fuzztime 1m  # one of the fuzz targets
tools/crosscheck.sh <path to hh_cli of hh-cpp>   # differential test against hh-cpp
go run ./cmd/hh-cli 0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed --out address.png
Directory Purpose
. the package, github.com/censync/go-hh, imported as hh
cmd/hh-cli command line tool with the options of hh_cli of hh-cpp; the differential test drives it
tools crosscheck.sh with its hand-made cases edge-cases.txt, and update-vectors.sh
testdata the golden vectors of hh-cpp and SOURCE, their provenance

The rules for patches are in CONTRIBUTING.md, the releases in CHANGELOG.md.

License

MIT, see LICENSE. Copyright (c) 2026 Dmitry Mandrika. CenSync

Documentation

Overview

Package hh implements Humanized Hash: it turns a blockchain address, a public key or any hash into a small deterministic picture that a person can compare at a glance, a 4 x 4 matrix of solid squares, circles and triangles in four colours. It exists to catch address poisoning and clipboard substitution, which work because people check only the ends of a long string.

A picture is made in three steps:

// Slow and public: cache the 32 bytes per input.
digest, err := hh.BaseDigestFromHex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
// One HMAC at most: hh.Universal(digest) or hh.Keyed(digest, key).
fp := hh.Universal(digest)
// Fast.
img, err := hh.Render(fp, 128, hh.RenderOptions{})

The base digest is the only slow value (16 384 iterations of PBKDF2-HMAC-SHA-256, a few milliseconds); it is public, and hosts cache its 32 bytes per input. A universal fingerprint is the same for everyone and is what two parties compare. A keyed fingerprint is one HMAC of the base digest under a 32-byte SecretKey: without the key nobody can compute, and therefore nobody can search for, a lookalike. Inside an application keyed pictures are the default.

An Image holds straight-alpha RGBA pixels; Image.NRGBA hands them to the standard image packages, and Image.EncodePNG, Image.EncodeBMP and Image.EncodeJPEG write files. Fingerprint.Tag gives six characters for a check that is certain where a picture is not, and Fingerprint.Layout describes the cells for hosts that draw vectors themselves.

The algorithm is frozen and has no version. This package follows the specification of hh-cpp, the reference implementation (https://github.com/censync/hh-cpp, docs/SPEC.md), and produces the same fingerprints, pixels and PNG, BMP and JPEG bytes as every other implementation. It uses integer arithmetic only and nothing beyond the standard library.

Every function is total: any input gives a result or one of the Err values of this package, which carry the numeric codes of the specification. Nothing panics. The package keeps no global state and is safe for concurrent use.

Example

The picture of an EVM address that everyone can compute.

package main

import (
	"fmt"

	hh "github.com/censync/go-hh"
)

func main() {
	digest, err := hh.BaseDigestFromHex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") // slow: cache it
	if err != nil {
		fmt.Println(err)
		return
	}
	fp := hh.Universal(digest)
	img, err := hh.Render(fp, 128, hh.RenderOptions{})
	if err != nil {
		fmt.Println(err)
		return
	}
	png, err := img.EncodePNG()
	if err != nil {
		fmt.Println(err)
		return
	}
	tag := fp.Tag()
	fmt.Printf("%d x %d pixels, %d bytes of PNG, tag %s-%s\n", img.Width, img.Height, len(png), tag[:3], tag[3:])
}
Output:
128 x 128 pixels, 4360 bytes of PNG, tag TKS-PVH
Example (DigestCache)

The base digest is the slow step and it is public, so a long-running program keeps it per input. The zero value of this cache is ready to use. Bound the map if the inputs come from outside.

package main

import (
	"fmt"
	"sync"

	hh "github.com/censync/go-hh"
)

type digestCache struct {
	mu      sync.Mutex
	digests map[string]hh.BaseDigest // keyed by the decoded input, not by its spelling
}

func (c *digestCache) ofAddress(address []byte) (hh.BaseDigest, error) {
	c.mu.Lock()
	d, ok := c.digests[string(address)]
	c.mu.Unlock()
	if ok {
		return d, nil
	}
	d, err := hh.NewBaseDigest(address) // outside the lock: this is the slow call
	if err != nil {
		return hh.BaseDigest{}, err
	}
	c.mu.Lock()
	if c.digests == nil {
		c.digests = make(map[string]hh.BaseDigest)
	}
	c.digests[string(address)] = d
	c.mu.Unlock()
	return d, nil
}

// The base digest is the slow step and it is public, so a long-running program
// keeps it per input. The zero value of this cache is ready to use. Bound the
// map if the inputs come from outside.
func main() {
	var cache digestCache
	address := []byte{
		0x5a, 0xAe, 0xb6, 0x05, 0x3F, 0x3E, 0x94, 0xC9, 0xb9, 0xA0,
		0x9f, 0x33, 0x66, 0x94, 0x35, 0xE7, 0xEf, 0x1B, 0xeA, 0xed,
	}
	first, err := cache.ofAddress(address)
	if err != nil {
		fmt.Println(err)
		return
	}
	again, _ := cache.ofAddress(address)
	_, err = cache.ofAddress(nil)
	fmt.Println(first == again, len(cache.digests), hh.Universal(first).Tag())
	fmt.Println(err)
}
Output:
true 1 TKSPVH
hh: empty_input: the input has no bytes

Index

Examples

Constants

View Source
const (
	// DigestSize is the size of a base digest in bytes.
	DigestSize = 32
	// MaxInputSize is the largest input in bytes (SPEC.md section 3).
	MaxInputSize = 1 << 20
)
View Source
const (
	MinJPEGQuality     = 50
	MaxJPEGQuality     = 100
	DefaultJPEGQuality = 92
)

The range of the JPEG quality and the value hosts should use when they have no reason for another.

View Source
const (
	// KeySize is the size of a secret key in bytes.
	KeySize = 32
	// KCVSize is the size of a key check value in bytes.
	KCVSize = 4
)
View Source
const (
	// MinSize is the smallest render size in pixels.
	MinSize = 16
	// MaxSize is the largest render size in pixels.
	MaxSize = 1024
)
View Source
const FingerprintSize = 32

FingerprintSize is the size of a fingerprint in bytes.

View Source
const MaxEncodeDimension = 4096

MaxEncodeDimension is the largest width and height the encoders accept.

View Source
const Version = "1.1.0"

Version is the version of this library. The algorithm itself has no version: no release changes a fingerprint, a pixel or an encoded byte.

Variables

View Source
var (
	// ErrEmptyInput reports an input without bytes.
	ErrEmptyInput = &Error{CodeEmptyInput, "the input has no bytes"}
	// ErrInputTooLarge reports an input longer than MaxInputSize bytes.
	ErrInputTooLarge = &Error{CodeInputTooLarge, "the input is longer than 1048576 bytes"}
	// ErrInvalidHex reports a string that is not an optional 0x followed by an
	// even, non-zero number of hexadecimal digits.
	ErrInvalidHex = &Error{CodeInvalidHex, "the string is not an even number of hexadecimal digits"}
	// ErrInvalidKey reports a key that is not 32 bytes, is all zero, is nil or
	// was closed.
	ErrInvalidKey = &Error{CodeInvalidKey, "the key must be 32 bytes that are not all zero"}
	// ErrInvalidDigest reports a stored base digest that is not 32 bytes.
	ErrInvalidDigest = &Error{CodeInvalidDigest, "the base digest must be 32 bytes"}
	// ErrInvalidFingerprint reports a fingerprint that is not 32 bytes, has an
	// unknown mode or is the zero value.
	ErrInvalidFingerprint = &Error{CodeInvalidFingerprint, "the fingerprint must be 32 bytes with a known mode"}
	// ErrInvalidSize reports a render size outside MinSize..MaxSize, or one
	// that leaves no room for the cells.
	ErrInvalidSize = &Error{CodeInvalidSize, "the image size must be 16..1024 and leave room for the cells"}
	// ErrInvalidFrame reports a frame style that does not fit the shape, such as
	// FrameTicks on a square picture or FrameRounded on a round one.
	ErrInvalidFrame = &Error{CodeInvalidFrame, "the frame is not allowed for this shape"}
	// ErrLowContrast reports an opaque background that is too close to a
	// palette colour.
	ErrLowContrast = &Error{CodeLowContrast, "the background is too close to a palette colour"}
	// ErrInvalidQuality reports a JPEG quality outside 50..100.
	ErrInvalidQuality = &Error{CodeInvalidQuality, "the JPEG quality must be 50..100"}
	// ErrInvalidImage reports image dimensions outside 1..4096 or a pixel
	// buffer of the wrong length.
	ErrInvalidImage = &Error{CodeInvalidImage, "the image dimensions or its buffer length are invalid"}
	// ErrInvalidArgument reports an unknown Shape or Frame value, a text that
	// is not valid UTF-8, or a string that a Parse function does not accept.
	ErrInvalidArgument = &Error{CodeInvalidArgument, "an unknown enumeration value, an ill-formed text or an unknown name"}
)

The errors of this package.

View Source
var White = RGB{0xFF, 0xFF, 0xFF}

White is the default background and the usual matte.

Functions

This section is empty.

Types

type Background

type Background struct {
	// contains filtered or unexported fields
}

Background is what lies behind the figures: an sRGB colour and an alpha. The zero value is the default, opaque white. Outside rounded or chamfered corners and outside the disc of the round shape a picture is always transparent. Backgrounds are comparable: equal values give equal pictures.

func Opaque

func Opaque(c RGB) Background

Opaque returns an opaque background. Render refuses one that is too close to a palette colour with ErrLowContrast.

func ParseBackground

func ParseBackground(s string) (Background, error)

ParseBackground parses eight hexadecimal digits of either case, "RRGGBBAA". It fails with ErrInvalidArgument.

func Translucent

func Translucent(c RGB, alpha uint8) Background

Translucent returns a background with an alpha from 0 (transparent) to 255 (opaque).

func Transparent

func Transparent() Background

Transparent returns the fully transparent background, for pictures laid over the host's own surface.

func (Background) Alpha

func (b Background) Alpha() uint8

Alpha returns the alpha of the background, 0 (transparent) to 255 (opaque).

func (Background) Color

func (b Background) Color() RGB

Color returns the colour of the background.

func (Background) MarshalText

func (b Background) MarshalText() ([]byte, error)

MarshalText returns the eight digits that String returns. It never fails.

func (Background) String

func (b Background) String() string

String returns the background as eight lowercase hexadecimal digits, "rrggbbaa", the form the golden vectors use.

func (*Background) UnmarshalText

func (b *Background) UnmarshalText(text []byte) error

UnmarshalText is ParseBackground. It fails with ErrInvalidArgument and then leaves the value as it was.

type BaseDigest

type BaseDigest [DigestSize]byte

BaseDigest is the stretched, public 32-byte value of an input (SPEC.md section 4). It is the only slow step, about 16 000 HMAC calls, so hosts cache it per input; both modes and any key derive their fingerprint from it cheaply. It needs no protection.

BaseDigest is an array: it is comparable, can be a map key and is stored as its 32 bytes (d[:]); ImportBaseDigest restores it.

func BaseDigestFromHex

func BaseDigestFromHex(s string) (BaseDigest, error)

BaseDigestFromHex computes the base digest of a binary input given as hexadecimal text: an optional "0x" or "0X", then an even, non-zero number of hexadecimal digits of either case. Every spelling of one address gives the same digest. It fails with ErrInvalidHex, or with ErrInputTooLarge for well-formed text that denotes more than MaxInputSize bytes.

Example

Every spelling of an address gives the same digest, and its 32 bytes are what a host caches.

package main

import (
	"fmt"

	hh "github.com/censync/go-hh"
)

func main() {
	a, _ := hh.BaseDigestFromHex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
	b, _ := hh.BaseDigestFromHex("5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED")
	fmt.Println(a == b)

	cached := a[:]
	restored, _ := hh.ImportBaseDigest(cached)
	fmt.Println(restored)
}
Output:
true
e212927148fcf76f6669c244a0db08bdd4f36dc50a378f6a1a3fe472807e7852

func BaseDigestFromText

func BaseDigestFromText(s string) (BaseDigest, error)

BaseDigestFromText computes the base digest of a text input: the UTF-8 bytes of s, without normalisation. A text input and a binary input with equal bytes give different digests. It fails with ErrEmptyInput or ErrInputTooLarge, and after the length check with ErrInvalidArgument if s is not valid UTF-8: such a string is not the encoding of any text, and hashing it would give a picture that no other implementation shows for the text it was meant to be.

Example

Formats that exist only as text, such as Bitcoin addresses, are hashed as text.

package main

import (
	"fmt"

	hh "github.com/censync/go-hh"
)

func main() {
	digest, err := hh.BaseDigestFromText("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
	fmt.Println(digest, err)
}
Output:
dc705192e4a205d8c403ae7693290df45f09cec04116ad38f6140f349392548f <nil>

func BaseDigestFromUTF8

func BaseDigestFromUTF8(text []byte) (BaseDigest, error)

BaseDigestFromUTF8 computes the base digest of a text input from its UTF-8 bytes, taken verbatim and not validated (SPEC.md section 3). It fails with ErrEmptyInput or ErrInputTooLarge.

func ImportBaseDigest

func ImportBaseDigest(b []byte) (BaseDigest, error)

ImportBaseDigest restores a cached digest from its 32 bytes. It fails with ErrInvalidDigest for any other length.

func NewBaseDigest

func NewBaseDigest(data []byte) (BaseDigest, error)

NewBaseDigest computes the base digest of a binary input: the bytes of an address, a public key or a hash, 1 to MaxInputSize of them. It fails with ErrEmptyInput or ErrInputTooLarge.

func (BaseDigest) String

func (d BaseDigest) String() string

String returns the digest as 64 lowercase hexadecimal digits.

type Cell

type Cell struct {
	// Figure is what the cell shows.
	Figure Figure
	// Color is the index of the figure's colour in Layout.Palette, 0..3. It
	// is 0 for an empty cell.
	Color uint8
}

Cell is one cell of the 4 x 4 matrix.

type Code

type Code int

Code is the numeric value of an error condition. The values are those of the C ABI of hh-cpp (SPEC.md section 14) and are the same in every implementation.

const (
	CodeOK                 Code = 0
	CodeEmptyInput         Code = 1
	CodeInputTooLarge      Code = 2
	CodeInvalidHex         Code = 3
	CodeInvalidKey         Code = 4
	CodeInvalidDigest      Code = 5
	CodeInvalidFingerprint Code = 6
	CodeInvalidSize        Code = 7
	CodeInvalidFrame       Code = 8
	CodeLowContrast        Code = 9
	CodeInvalidQuality     Code = 10
	CodeInvalidImage       Code = 11
	CodeBufferTooSmall     Code = 12
	CodeOutOfMemory        Code = 13
	CodeInvalidArgument    Code = 14
)

The error codes of SPEC.md section 14. CodeBufferTooSmall and CodeOutOfMemory exist for completeness of the table: this package allocates its results itself and never reports them.

func (Code) String

func (c Code) String() string

String returns the name of the code as the specification and the golden vectors spell it, for example "invalid_hex", or "unknown" for a value outside the table.

type ContrastReport

type ContrastReport struct {
	// FiguresX100 is the contrast of the weakest palette colour against the
	// background.
	FiguresX100 int
	// FrameX100 is the contrast of the frame against the background. It is
	// informative.
	FrameX100 int
}

ContrastReport holds WCAG contrast ratios times 100, rounded down: 300 means 3:1.

func MeasureContrast

func MeasureContrast(opts RenderOptions, page RGB) ContrastReport

MeasureContrast measures what the options give over a page of the colour page; for an opaque background the page does not matter. Render refuses an opaque background with FiguresX100 below 200; hosts should warn below 300. For a translucent background pass the colour of the surface underneath.

Example

Figures that vanish into the background make different pictures look alike, so a host that lets users choose a background measures it first.

package main

import (
	"fmt"

	hh "github.com/censync/go-hh"
)

func main() {
	page := hh.RGB{R: 0x12, G: 0x12, B: 0x12} // what the host paints underneath
	for _, background := range []hh.Background{
		hh.Transparent(),
		hh.Opaque(hh.RGB{R: 0xF2, G: 0xF2, B: 0xF2}),
		hh.Opaque(hh.RGB{R: 0x9E, G: 0x9E, B: 0x9E}),
	} {
		report := hh.MeasureContrast(hh.RenderOptions{Background: background}, page)
		switch {
		case report.FiguresX100 < 200:
			fmt.Printf("%v: %d, Render refuses it if it is opaque\n", background, report.FiguresX100)
		case report.FiguresX100 < 300:
			fmt.Printf("%v: %d, warn the user\n", background, report.FiguresX100)
		default:
			fmt.Printf("%v: %d\n", background, report.FiguresX100)
		}
	}
}
Output:
00000000: 300
f2f2f2ff: 268, warn the user
9e9e9eff: 112, Render refuses it if it is opaque

type Error

type Error struct {
	// contains filtered or unexported fields
}

Error is the type of every error this package returns. The package returns only its Err values, never a copy or a wrapped one, so errors can be compared with == or errors.Is, and errors.As gives access to the numeric code.

Example

Errors are values: compare them, or read the numeric code that every implementation of hh shares.

package main

import (
	"errors"
	"fmt"

	hh "github.com/censync/go-hh"
)

func main() {
	_, err := hh.BaseDigestFromHex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAe") // one digit short
	fmt.Println(errors.Is(err, hh.ErrInvalidHex))

	var e *hh.Error
	if errors.As(err, &e) {
		fmt.Println(int(e.Code()), e.Code())
	}
}
Output:
true
3 invalid_hex

func (*Error) Code

func (e *Error) Code() Code

Code returns the numeric code of SPEC.md section 14. A nil *Error is no error: its code is CodeOK.

func (*Error) Error

func (e *Error) Error() string

Error returns the text "hh: <name>: <description>", and "hh: ok" for a nil *Error.

type Figure

type Figure uint8

Figure is what a cell shows. The values are the layout values of SPEC.md section 5.1.

const (
	FigureNone          Figure = 0 // an empty cell
	FigureSquare        Figure = 1 // the full cell
	FigureCircle        Figure = 2 // the disc inscribed in the cell
	FigureTriangleUp    Figure = 3 // base on the bottom side
	FigureTriangleRight Figure = 4 // base on the left side
	FigureTriangleDown  Figure = 5 // base on the top side
	FigureTriangleLeft  Figure = 6 // base on the right side
)

The figures of SPEC.md section 5.1. A triangle is isosceles: its base is one full side of the cell and its apex the midpoint of the opposite side.

func (Figure) String

func (f Figure) String() string

String returns the name of the figure, for example "triangle_up", or "invalid" for a value outside the table.

type Fingerprint

type Fingerprint struct {
	// contains filtered or unexported fields
}

Fingerprint is 32 bytes and the mode they were derived in: everything a picture depends on. It is comparable and can be a map key. The zero value is not a fingerprint: Render refuses it with ErrInvalidFingerprint.

func ImportFingerprint

func ImportFingerprint(b []byte, mode Mode) (Fingerprint, error)

ImportFingerprint is for hosts that compute the keyed HMAC elsewhere, for example inside a secure element: it takes the 32 fingerprint bytes and the mode they belong to. It fails with ErrInvalidFingerprint for any other length or an unknown mode.

Example

A host that computes the keyed HMAC elsewhere, for example in a secure element, imports the result and never creates a SecretKey.

package main

import (
	"fmt"

	hh "github.com/censync/go-hh"
)

func main() {
	fromElsewhere := make([]byte, hh.FingerprintSize)
	fp, err := hh.ImportFingerprint(fromElsewhere, hh.ModeKeyed)
	fmt.Println(fp.Mode(), fp.Tag(), err)

	_, err = hh.ImportFingerprint(fromElsewhere[:16], hh.ModeKeyed)
	fmt.Println(err)
}
Output:
keyed 000000 <nil>
hh: invalid_fingerprint: the fingerprint must be 32 bytes with a known mode

func Keyed

func Keyed(d BaseDigest, key *SecretKey) (Fingerprint, error)

Keyed returns the keyed fingerprint: one HMAC of the base digest under the key. It fails with ErrInvalidKey if the key is nil or was closed.

Example

The private picture: only holders of the key can compute it.

package main

import (
	"fmt"

	hh "github.com/censync/go-hh"
)

// The key of the examples. A real key is uniformly random or the output of a
// key derivation function, and never appears in source code.
var exampleKey = []byte("an example key, 32 bytes long...")

func main() {
	key, err := hh.NewSecretKey(exampleKey)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer key.Close()

	digest, _ := hh.BaseDigestFromHex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
	fp, err := hh.Keyed(digest, key)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("%v %s, key check value %x\n", fp.Mode(), fp.Tag(), key.KCV())
}
Output:
keyed 1KKSCS, key check value 6ae70605

func Universal

func Universal(d BaseDigest) Fingerprint

Universal returns the universal fingerprint, which is the base digest itself.

func (Fingerprint) Bytes

func (fp Fingerprint) Bytes() [FingerprintSize]byte

Bytes returns the 32 bytes.

func (Fingerprint) IsZero

func (fp Fingerprint) IsZero() bool

IsZero reports whether fp is the zero value, which no function of this package returns without an error.

func (Fingerprint) Layout

func (fp Fingerprint) Layout() Layout

Layout returns the cells, the palette and the mode. The zero Fingerprint gives 16 empty cells.

Example

A host that draws vectors itself reads the cells instead of the pixels.

package main

import (
	"fmt"

	hh "github.com/censync/go-hh"
)

func main() {
	digest, _ := hh.BaseDigestFromHex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
	layout := hh.Universal(digest).Layout()
	for i, cell := range layout.Cells {
		if cell.Figure == hh.FigureNone {
			fmt.Printf("[%21s]", "")
		} else {
			fmt.Printf("[%-14v %v]", cell.Figure, layout.Palette[cell.Color])
		}
		if i%4 == 3 {
			fmt.Println()
		}
	}
}
Output:
[triangle_left  7a96c5][                     ][triangle_up    c10445][circle         c10445]
[square         890af0][triangle_left  d48200][triangle_left  c10445][circle         890af0]
[circle         7a96c5][circle         890af0][triangle_down  7a96c5][square         7a96c5]
[triangle_right 7a96c5][triangle_down  d48200][                     ][triangle_right d48200]

func (Fingerprint) Mode

func (fp Fingerprint) Mode() Mode

Mode returns the mode of the fingerprint, or 0 for the zero value.

func (Fingerprint) Tag

func (fp Fingerprint) Tag() string

Tag returns the six-character Crockford Base32 tag, for example "K7QM2X"; hosts display it grouped as "K7Q-M2X". Text allows a certain check where a picture does not. The tag of a keyed fingerprint depends on the key.

type Frame

type Frame uint8

Frame is the frame style of a picture. Every style is open to universal and keyed fingerprints alike; what limits it is the shape. FrameNone, FramePlain, FrameDouble and FrameThick fit both shapes, FrameRounded, FrameChamfered and FrameBrackets need ShapeSquare, FrameTicks and FrameGaps need ShapeRound, and Render refuses a style that does not fit the shape with ErrInvalidFrame. Only FrameAutomatic looks at the mode: it gives keyed square pictures rounded corners. A host that marks its keyed pictures with a frame picks the style.

const (
	FrameAutomatic Frame = 0 // keyed and square: FrameRounded; otherwise FrameNone
	FrameNone      Frame = 1 // either shape: no frame
	FramePlain     Frame = 2 // either shape: a thin square frame, or a thin ring
	FrameRounded   Frame = 3 // square only: rounded corners
	FrameChamfered Frame = 4 // square only: four cut corners
	FrameDouble    Frame = 5 // either shape: two thin lines
	FrameThick     Frame = 6 // either shape: one line three times as thick
	FrameBrackets  Frame = 7 // square only: corner brackets
	FrameTicks     Frame = 8 // round only: a ring with four ticks
	FrameGaps      Frame = 9 // round only: a ring with four gaps
)

The frame styles of SPEC.md section 6, each open to both modes.

func ParseFrame

func ParseFrame(name string) (Frame, error)

ParseFrame is the inverse of Frame.String. It fails with ErrInvalidArgument.

func (Frame) MarshalText

func (f Frame) MarshalText() ([]byte, error)

MarshalText returns the name that String returns. It fails with ErrInvalidArgument for a value outside the table.

func (Frame) String

func (f Frame) String() string

String returns the name of SPEC.md section 6, for example "double", or "invalid" for a value outside the table.

func (*Frame) UnmarshalText

func (f *Frame) UnmarshalText(text []byte) error

UnmarshalText is ParseFrame. It fails with ErrInvalidArgument and then leaves the value as it was.

type Image

type Image struct {
	// Width and Height are the dimensions in pixels.
	Width, Height int
	// Pix holds the pixels: R, G, B, A of the top left pixel, then the rest
	// of its row, then the rows below.
	Pix []byte
}

Image is pixels, not a picture: Width x Height pixels, row-major from the top left, 4 bytes per pixel in the order R, G, B, A with straight (non-premultiplied) alpha, the layout of image.NRGBA.

Render returns square images. The encoders take any Image of 1..4096 by 1..4096 pixels whose Pix has Width * Height * 4 bytes and fail with ErrInvalidImage otherwise. They are deterministic: the same image gives the same bytes in every implementation of hh.

func Render

func Render(fp Fingerprint, size int, opts RenderOptions) (*Image, error)

Render draws the fingerprint as size x size pixels. The checks are made in the order of SPEC.md section 6: ErrInvalidFingerprint for the zero Fingerprint, ErrInvalidArgument for an unknown Shape or Frame value, ErrInvalidSize unless size is MinSize..MaxSize, ErrInvalidFrame if the frame does not fit the shape (the mode plays no part), ErrLowContrast for an opaque background with less than 2:1 against a palette colour, and ErrInvalidSize again if the size leaves no room for the cells (the round shape with FrameDouble or FrameThick below 18 pixels).

Render at the exact device-pixel size instead of scaling a picture: the rasteriser is anti-aliased for the size it is asked for.

Example

A round picture with a double frame on a dark surface. Every style that fits the shape is open to both modes; one that does not is refused.

package main

import (
	"fmt"

	hh "github.com/censync/go-hh"
)

// The key of the examples. A real key is uniformly random or the output of a
// key derivation function, and never appears in source code.
var exampleKey = []byte("an example key, 32 bytes long...")

func main() {
	key, _ := hh.NewSecretKey(exampleKey)
	defer key.Close()
	digest, _ := hh.BaseDigestFromHex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
	fp, _ := hh.Keyed(digest, key)

	opts := hh.RenderOptions{
		Shape:      hh.ShapeRound,
		Frame:      hh.FrameDouble,
		Background: hh.Opaque(hh.RGB{R: 0x12, G: 0x12, B: 0x12}),
	}
	img, err := hh.Render(fp, 96, opts)
	fmt.Println(img.Width, img.Height, len(img.Pix), err)

	// The same look for the public picture.
	img, err = hh.Render(hh.Universal(digest), 96, opts)
	fmt.Println(img.Width, img.Height, len(img.Pix), err)

	// Rounded corners need the square shape.
	opts.Frame = hh.FrameRounded
	_, err = hh.Render(fp, 96, opts)
	fmt.Println(err)
}
Output:
96 96 36864 <nil>
96 96 36864 <nil>
hh: invalid_frame: the frame is not allowed for this shape

func (*Image) EncodeBMP

func (m *Image) EncodeBMP(matte RGB) ([]byte, error)

EncodeBMP encodes the image as a 24-bit BMP (SPEC.md section 12). BMP carries no alpha here, so every pixel is laid over the matte; White is the usual one. It fails with ErrInvalidImage.

func (*Image) EncodeJPEG

func (m *Image) EncodeJPEG(quality int, matte RGB) ([]byte, error)

EncodeJPEG encodes the image as baseline JFIF, 8 bits per sample, 4:4:4, with the tables of ITU-T T.81 annex K (SPEC.md section 13). JPEG has no alpha, so every pixel is laid over the matte; White is the usual one. It fails with ErrInvalidImage, then with ErrInvalidQuality unless quality is 50..100.

JPEG is offered for compatibility; it rings on flat colour edges. Prefer PNG or the pixels themselves.

func (*Image) EncodePNG

func (m *Image) EncodePNG() ([]byte, error)

EncodePNG encodes the image as an 8-bit truecolour PNG, with an alpha channel only if some pixel is not opaque (SPEC.md section 11). It fails with ErrInvalidImage.

func (*Image) NRGBA

func (m *Image) NRGBA() *image.NRGBA

NRGBA returns the image as an *image.NRGBA for the standard image packages. The result shares Pix with m. An Image that the encoders would refuse gives an empty image.

Example

The pixels have the layout of image.NRGBA, so the standard image packages take them as they are.

package main

import (
	"fmt"
	"image"
	"image/draw"

	hh "github.com/censync/go-hh"
)

func main() {
	digest, _ := hh.BaseDigestFromHex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
	img, _ := hh.Render(hh.Universal(digest), 64, hh.RenderOptions{Background: hh.Transparent()})

	canvas := image.NewRGBA(image.Rect(0, 0, 200, 80))
	draw.Draw(canvas, image.Rect(8, 8, 72, 72), img.NRGBA(), image.Point{}, draw.Over)
	fmt.Println(canvas.RGBAAt(8+10, 8+10), canvas.RGBAAt(8+31, 8+31))
}
Output:
{122 150 197 255} {0 0 0 0}

type Layout

type Layout struct {
	// Mode is the mode of the fingerprint.
	Mode Mode
	// Cells are the 16 cells, row-major from the top left.
	Cells [16]Cell
	// Palette holds the four colours of the figures.
	Palette [4]RGB
	// FrameColor is the colour of the frame.
	FrameColor RGB
}

Layout is what a fingerprint shows, for hosts that draw vectors themselves. Cells are row-major from the top left. The raster of Render is the canonical form and the only one covered by byte-exact vectors.

type Mode

type Mode uint8

Mode tells how a fingerprint was derived. Universal pictures are the same for everyone; keyed pictures can be computed only with the secret key. The two pictures of one input are unrelated.

const (
	// ModeUniversal is the mode of the public picture, the one two parties
	// compare.
	ModeUniversal Mode = 1
	// ModeKeyed is the mode of the private picture, the default inside an
	// application.
	ModeKeyed Mode = 2
)

func ParseMode

func ParseMode(name string) (Mode, error)

ParseMode is the inverse of Mode.String: it takes "universal" or "keyed" and fails with ErrInvalidArgument.

func (Mode) MarshalText

func (m Mode) MarshalText() ([]byte, error)

MarshalText returns the name that String returns, for a host that stores the mode beside the bytes of a fingerprint. It fails with ErrInvalidArgument for any value other than ModeUniversal and ModeKeyed.

func (Mode) String

func (m Mode) String() string

String returns "universal" or "keyed", or "invalid" for any other value.

func (*Mode) UnmarshalText

func (m *Mode) UnmarshalText(text []byte) error

UnmarshalText is ParseMode. It fails with ErrInvalidArgument and then leaves the value as it was.

type Opacity

type Opacity struct {
	// contains filtered or unexported fields
}

Opacity is an alpha value whose zero value is fully opaque; Alpha makes any other.

func Alpha

func Alpha(alpha uint8) Opacity

Alpha returns the opacity with the given alpha, 0 (transparent) to 255 (opaque).

func ParseOpacity

func ParseOpacity(s string) (Opacity, error)

ParseOpacity parses an alpha, 0 to 255, written as ASCII decimal digits without a sign. It fails with ErrInvalidArgument.

func (Opacity) Alpha

func (o Opacity) Alpha() uint8

Alpha returns the alpha value, 255 for the zero Opacity.

func (Opacity) MarshalText

func (o Opacity) MarshalText() ([]byte, error)

MarshalText returns the number that String returns. It never fails.

func (Opacity) String

func (o Opacity) String() string

String returns the alpha as a decimal number, "0" to "255".

func (*Opacity) UnmarshalText

func (o *Opacity) UnmarshalText(text []byte) error

UnmarshalText is ParseOpacity. It fails with ErrInvalidArgument and then leaves the value as it was.

type RGB

type RGB struct {
	// R, G and B are the red, green and blue channels.
	R, G, B uint8
}

RGB is an sRGB colour.

func ParseRGB

func ParseRGB(s string) (RGB, error)

ParseRGB parses six hexadecimal digits of either case, "RRGGBB". It fails with ErrInvalidArgument.

func (RGB) MarshalText

func (c RGB) MarshalText() ([]byte, error)

MarshalText returns the six digits that String returns. It never fails.

func (RGB) String

func (c RGB) String() string

String returns the colour as six lowercase hexadecimal digits, "rrggbb".

func (*RGB) UnmarshalText

func (c *RGB) UnmarshalText(text []byte) error

UnmarshalText is ParseRGB. It fails with ErrInvalidArgument and then leaves the value as it was.

type RenderOptions

type RenderOptions struct {
	// Shape is square or round.
	Shape Shape
	// Frame is the frame style: any style that fits the shape, in either mode.
	// A host that marks its keyed pictures with a frame uses one style
	// everywhere, since a marker is only useful if it is familiar, and names
	// the mode in the caption, since a frame alone proves nothing.
	Frame Frame
	// Background is the colour and alpha behind the figures.
	Background Background
	// FrameAlpha is the alpha of the frame; the frame colour itself is fixed.
	FrameAlpha Opacity
}

RenderOptions chooses the look of a render; the cells, the palette and the geometry are fixed by the specification. The zero value is the default: a square picture on opaque white with the automatic frame.

Every field is an encoding.TextMarshaler and an encoding.TextUnmarshaler in the text form of its String method and its Parse function, so the options pass unchanged through encoding/json and every other format that is built on those interfaces:

{"Shape":"round","Frame":"double","Background":"121212ff","FrameAlpha":"200"}

A field that is missing keeps its default. A text that the Parse function of the field refuses is ErrInvalidArgument.

Example

The look of a program in its configuration file. Every field of RenderOptions reads and writes the names and the hexadecimal forms of the specification.

package main

import (
	"encoding/json"
	"errors"
	"fmt"

	hh "github.com/censync/go-hh"
)

func main() {
	type config struct {
		PictureSize int
		Picture     hh.RenderOptions
	}
	file := []byte(`{"PictureSize": 96, "Picture": {"Shape": "round", "Frame": "double", "Background": "121212ff"}}`)
	var c config
	if err := json.Unmarshal(file, &c); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(c.Picture.Shape, c.Picture.Frame, c.Picture.Background, c.Picture.FrameAlpha)

	written, _ := json.Marshal(c.Picture)
	fmt.Println(string(written))

	err := json.Unmarshal([]byte(`{"Picture": {"Frame": "dotted"}}`), &c)
	fmt.Println(errors.Is(err, hh.ErrInvalidArgument))
}
Output:
round double 121212ff 255
{"Shape":"round","Frame":"double","Background":"121212ff","FrameAlpha":"255"}
true

type SecretKey

type SecretKey struct {
	// contains filtered or unexported fields
}

SecretKey is the 32-byte secret of keyed mode. The key must be uniformly random or the output of a key derivation function; there is no passphrase form.

Close overwrites the key with zeros. That is as far as a garbage-collected runtime lets a library go: the copies that crypto/hmac makes while a fingerprint is computed, and any copy the runtime made when it moved a stack, stay in memory until they are reused. A host that must keep the key out of the Go heap computes the keyed HMAC elsewhere and uses ImportFingerprint.

A SecretKey is safe for concurrent use, including Close. Pass the pointer. A copy of the struct is a second handle on the same key, not a second key: closing either closes both.

No printer shows the bytes. A *SecretKey prints as a placeholder through Format and String. A SecretKey value, which has neither method, prints as the address of a function, and so does a key in an unexported field of another struct. Packages that dump values by reflection, unexported fields included, find the same function value and nothing behind it, and encoding/json writes {}.

func NewSecretKey

func NewSecretKey(b []byte) (*SecretKey, error)

NewSecretKey accepts exactly 32 bytes that are not all zero; anything else is ErrInvalidKey. A zero-filled buffer is what a failed key load looks like and must never produce pictures. The bytes are copied: wipe your own slice.

func (*SecretKey) Close

func (k *SecretKey) Close() error

Close overwrites the key with zeros; every later use of the key fails with ErrInvalidKey, except KCV. Closing twice, or closing a nil key, is harmless. The error is always nil; the signature is that of io.Closer.

func (*SecretKey) Format

func (k *SecretKey) Format(f fmt.State, verb rune)

Format prints a placeholder for every verb that fmt hands to a Formatter, so that a key never reaches a log through the fmt package. The verbs %T and %p, which fmt answers itself, give the type and the address.

func (*SecretKey) KCV

func (k *SecretKey) KCV() [KCVSize]byte

KCV returns the key check value: 4 bytes a host stores beside its cached data to notice that the key, and with it every keyed picture, changed. It is public: it reveals nothing useful about the key, it is computed when the key is created and it stays available after Close. The KCV of a nil key is all zero.

func (*SecretKey) String

func (k *SecretKey) String() string

String returns the placeholder that Format prints, for the printers that look for a fmt.Stringer and not for a fmt.Formatter.

type Shape

type Shape uint8

Shape is the outline of the picture.

const (
	// ShapeSquare is the default.
	ShapeSquare Shape = 0
	// ShapeRound inscribes the 4 x 4 grid in a circle; no cell is clipped, the
	// cells are smaller.
	ShapeRound Shape = 1
)

func ParseShape

func ParseShape(name string) (Shape, error)

ParseShape is the inverse of Shape.String. It fails with ErrInvalidArgument.

func (Shape) MarshalText

func (s Shape) MarshalText() ([]byte, error)

MarshalText returns the name that String returns, so that a Shape is a name in JSON and in every other format that knows encoding.TextMarshaler. It fails with ErrInvalidArgument for a value outside the table.

func (Shape) String

func (s Shape) String() string

String returns the name of SPEC.md section 6, "square" or "round", or "invalid" for any other value.

func (*Shape) UnmarshalText

func (s *Shape) UnmarshalText(text []byte) error

UnmarshalText is ParseShape. It fails with ErrInvalidArgument and then leaves the value as it was.

Directories

Path Synopsis
cmd
hh-cli command
Command hh-cli renders the picture of an address or a hash.
Command hh-cli renders the picture of an address or a hash.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL