Bech32 Addresses

--

At the core of Bitcoin is the usage of the Bitcoin address, and which defines the sender and recipient of Bitcoin transactions. The newest format is known as SegWit address format — and is specified here: https://en.bitcoin.it/wiki/BIP_0readable, and is defined as the BIP (Bitcoin Improvement Protocol) 173. With Bitcoin, these addresses start with “bc1” and use a Bech32 encoding format.

A core advantage of using a bech32 address is that it includes an error-correction code that can correct many address typos, whereas legacy Bitcoin addresses could send the transaction to the wrong address.

The Bech32 addresses are human readable. The first part is 1 to 83 US-ASCII characters, with each character in the range of 33 to 126. There is then a “1” as a seperate, and followed by the data part which is alphanumeric characters that exclude “1”, “b”, “i”, and “o”:

000 001 010 011 100 101 110 111
--------------------------------------
00 | q p z r y 9 x 8
01 | g f 2 t v d w 0
10 | s 3 j n 5 4 k h
11 | c e 6 m u a 7 l

With AGE (Actually Good Encryption), the private key for Ed25519 has a prefix of “AGE-SECRET-KEY-”, and the public key has the prefix of “age”. These are defined as “HRP” — the human readable part — elements. After this, there is a seperator of a “1”, and followed by the key data.

A sample run is [here]:

Private key AGE-SECRET-KEY-1WJFU3W02V65ARQQVS28V6TVHK96AG6PMLGUU0L5WX87UUR5NC7GSU6N5AU
Private key Decoded prefix: AGE-SECRET-KEY-
Private key Decoded Data: 7493c8b9ea66a9d1800c828ecd2d97b175d4683bfa39c7fe8e31fdce0e93c791
Public key age1nfqma603nvmau8lqc8l94e9kppzlft3cdw03x0nk82tuxstuhvpq6apcvm
Private key Decoded prefix: age
Private key Decoded Data: 9a41bee9f19b37de1fe0c1fe5ae4b60845f4ae386b9f133e763a97c3417cbb02

In this case we can see that the private key for Ed25519 is 7493c8b9ea66a9d1800c828ecd2d97b175d4683bfa39c7fe8e31fdce0e93c791, and which has 32 bytes. This gives us a 256-bit private key. The public key is 9a41bee9f19b37de1fe0c1fe5ae4b60845f4ae386b9f133e763a97c3417cbb02, and which also has 32 bytes. This gives a 256-bit public key.

Note that the last six six characters of the data part define the checksum.

The coding is [here]:

The coding used uses some of the code from the Age GitHub site:

package main

import (
"fmt"

"encoding/hex"
"strings"

"filippo.io/age"
)

// Encode encodes the HRP and a bytes slice to Bech32. If the HRP is uppercase,
// the output will be uppercase.

var charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"

var generator = []uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}

func polymod(values []byte) uint32 {
chk := uint32(1)
for _, v := range values {
top := chk >> 25
chk = (chk & 0x1ffffff) << 5
chk = chk ^ uint32(v)
for i := 0; i < 5; i++ {
bit := top >> i & 1
if bit == 1 {
chk ^= generator[i]
}
}
}
return chk
}

func hrpExpand(hrp string) []byte {
h := []byte(strings.ToLower(hrp))
var ret []byte
for _, c := range h {
ret = append(ret, c>>5)
}
ret = append(ret, 0)
for _, c := range h {
ret = append(ret, c&31)
}
return ret
}
func createChecksum(hrp string, data []byte) []byte {
values := append(hrpExpand(hrp), data...)
values = append(values, []byte{0, 0, 0, 0, 0, 0}...)
mod := polymod(values) ^ 1
ret := make([]byte, 6)
for p := range ret {
shift := 5 * (5 - p)
ret[p] = byte(mod>>shift) & 31
}
return ret
}
func convertBits(data []byte, frombits, tobits byte, pad bool) ([]byte, error) {
var ret []byte
acc := uint32(0)
bits := byte(0)
maxv := byte(1<<tobits - 1)
for idx, value := range data {
if value>>frombits != 0 {
return nil, fmt.Errorf("invalid data range: data[%d]=%d (frombits=%d)", idx, value, frombits)
}
acc = acc<<frombits | uint32(value)
bits += frombits
for bits >= tobits {
bits -= tobits
ret = append(ret, byte(acc>>bits)&maxv)
}
}
if pad {
if bits > 0 {
ret = append(ret, byte(acc<<(tobits-bits))&maxv)
}
} else if bits >= frombits {
return nil, fmt.Errorf("illegal zero padding")
} else if byte(acc<<(tobits-bits))&maxv != 0 {
return nil, fmt.Errorf("non-zero padding")
}
return ret, nil
}
func verifyChecksum(hrp string, data []byte) bool {
return polymod(append(hrpExpand(hrp), data...)) == 1
}

// Decode decodes a Bech32 string. If the string is uppercase, the HRP will be uppercase.
func Decode(s string) (hrp string, data []byte, err error) {
if strings.ToLower(s) != s && strings.ToUpper(s) != s {
return "", nil, fmt.Errorf("mixed case")
}
pos := strings.LastIndex(s, "1")
if pos < 1 || pos+7 > len(s) {
return "", nil, fmt.Errorf("separator '1' at invalid position: pos=%d, len=%d", pos, len(s))
}
hrp = s[:pos]
for p, c := range hrp {
if c < 33 || c > 126 {
return "", nil, fmt.Errorf("invalid character human-readable part: s[%d]=%d", p, c)
}
}
s = strings.ToLower(s)
for p, c := range s[pos+1:] {
d := strings.IndexRune(charset, c)
if d == -1 {
return "", nil, fmt.Errorf("invalid character data part: s[%d]=%v", p, c)
}
data = append(data, byte(d))
}
if !verifyChecksum(hrp, data) {
return "", nil, fmt.Errorf("invalid checksum")
}
data, err = convertBits(data[:len(data)-6], 5, 8, false)
if err != nil {
return "", nil, err
}
return hrp, data, nil
}
func Encode(hrp string, data []byte) (string, error) {
values, err := convertBits(data, 8, 5, true)
if err != nil {
return "", err
}
if len(hrp) < 1 {
return "", fmt.Errorf("invalid HRP: %q", hrp)
}
for p, c := range hrp {
if c < 33 || c > 126 {
return "", fmt.Errorf("invalid HRP character: hrp[%d]=%d", p, c)
}
}
if strings.ToUpper(hrp) != hrp && strings.ToLower(hrp) != hrp {
return "", fmt.Errorf("mixed case HRP: %q", hrp)
}
lower := strings.ToLower(hrp) == hrp
hrp = strings.ToLower(hrp)
var ret strings.Builder
ret.WriteString(hrp)
ret.WriteString("1")
for _, p := range values {
ret.WriteByte(charset[p])
}
for _, p := range createChecksum(hrp, values) {
ret.WriteByte(charset[p])
}
if lower {
return ret.String(), nil
}
return strings.ToUpper(ret.String()), nil
}
func main() {

identity, _ := age.GenerateX25519Identity()
publicKey := identity.Recipient().String()
privateKey := identity.String()

prefix, data, _ := Decode(privateKey)

fmt.Printf("Private key %v\n", privateKey)
fmt.Println("Private key Decoded prefix:", prefix)
fmt.Println("Private key Decoded Data:", hex.EncodeToString(data))

prefix, data, _ = Decode(publicKey)

fmt.Printf("\nPublic key %v\n", publicKey)
fmt.Println("Private key Decoded prefix:", prefix)
fmt.Println("Private key Decoded Data:", hex.EncodeToString(data))

}

--

--

Prof Bill Buchanan OBE FRSE
ASecuritySite: When Bob Met Alice

Professor of Cryptography. Serial innovator. Believer in fairness, justice & freedom. Based in Edinburgh. Old World Breaker. New World Creator. Building trust.