Golang Age Bech32 Address FormatWith Age encryption, the key format uses the Bech32, and which is human readable. The private key 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. After ths, there is a seperator of a "1", and followed by the key data. |
Key formats
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: [link], and is defined as the BIP (Bitcoin Improvement Protocol) 173. With Bitcoin, these addresses start with “bc1” and use a Bech32 encoding format.
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:
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.
<Coding
The coding used uses some of the code from the Age GitHub site [here]:
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("Public key Decoded prefix:", prefix) fmt.Println("Public key Decoded Data:", hex.EncodeToString(data)) }