nip19.go raw

   1  package nostr
   2  
   3  import (
   4  	"encoding/binary"
   5  	"encoding/hex"
   6  	"fmt"
   7  	"strings"
   8  )
   9  
  10  // Nevent returns the NIP-19 bech32-encoded nevent string for an event,
  11  // including relay hints. This is the shareable identifier that Nostr
  12  // clients use to locate and display an event.
  13  func Nevent(eventIDHex string, relays []string, authorHex string, kind int) (string, error) {
  14  	id, err := hex.DecodeString(eventIDHex)
  15  	if err != nil || len(id) != 32 {
  16  		return "", fmt.Errorf("invalid event id hex")
  17  	}
  18  
  19  	var tlv []byte
  20  
  21  	// TLV type 0: event id (32 bytes).
  22  	tlv = append(tlv, 0, 32)
  23  	tlv = append(tlv, id...)
  24  
  25  	// TLV type 1: relay URL(s).
  26  	for _, r := range relays {
  27  		b := []byte(r)
  28  		tlv = append(tlv, 1, byte(len(b)))
  29  		tlv = append(tlv, b...)
  30  	}
  31  
  32  	// TLV type 2: author pubkey (32 bytes).
  33  	if authorHex != "" {
  34  		pub, err := hex.DecodeString(authorHex)
  35  		if err == nil && len(pub) == 32 {
  36  			tlv = append(tlv, 2, 32)
  37  			tlv = append(tlv, pub...)
  38  		}
  39  	}
  40  
  41  	// TLV type 3: kind (4 bytes big-endian).
  42  	var kb [4]byte
  43  	binary.BigEndian.PutUint32(kb[:], uint32(kind))
  44  	tlv = append(tlv, 3, 4)
  45  	tlv = append(tlv, kb[:]...)
  46  
  47  	return bech32Encode("nevent", tlv)
  48  }
  49  
  50  // NpubToHex decodes an npub1... bech32 string to a 32-byte hex pubkey.
  51  func NpubToHex(npub string) (string, error) {
  52  	hrp, data, err := bech32Decode(npub)
  53  	if err != nil {
  54  		return "", fmt.Errorf("bech32 decode: %w", err)
  55  	}
  56  	if hrp != "npub" {
  57  		return "", fmt.Errorf("expected hrp 'npub', got '%s'", hrp)
  58  	}
  59  	bytes, err := convertBits(data, 5, 8, false)
  60  	if err != nil {
  61  		return "", fmt.Errorf("convert bits: %w", err)
  62  	}
  63  	if len(bytes) != 32 {
  64  		return "", fmt.Errorf("expected 32 bytes, got %d", len(bytes))
  65  	}
  66  	return hex.EncodeToString(bytes), nil
  67  }
  68  
  69  // PubkeyToNpub encodes a 32-byte hex pubkey to npub1... bech32 format.
  70  func PubkeyToNpub(hexPub string) (string, error) {
  71  	b, err := hex.DecodeString(hexPub)
  72  	if err != nil {
  73  		return "", err
  74  	}
  75  	if len(b) != 32 {
  76  		return "", fmt.Errorf("expected 32 bytes, got %d", len(b))
  77  	}
  78  	return bech32Encode("npub", b)
  79  }
  80  
  81  // --- bech32 encoding/decoding (BIP-173) ---
  82  
  83  const bech32Charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
  84  
  85  func bech32Encode(hrp string, data []byte) (string, error) {
  86  	// Convert 8-bit data to 5-bit groups.
  87  	conv, err := convertBits(data, 8, 5, true)
  88  	if err != nil {
  89  		return "", err
  90  	}
  91  
  92  	// Compute checksum.
  93  	chk := bech32Checksum(hrp, conv)
  94  
  95  	var b strings.Builder
  96  	b.WriteString(hrp)
  97  	b.WriteByte('1')
  98  	for _, d := range conv {
  99  		b.WriteByte(bech32Charset[d])
 100  	}
 101  	for _, d := range chk {
 102  		b.WriteByte(bech32Charset[d])
 103  	}
 104  	return b.String(), nil
 105  }
 106  
 107  func bech32Decode(s string) (string, []byte, error) {
 108  	s = strings.ToLower(s)
 109  	pos := strings.LastIndex(s, "1")
 110  	if pos < 1 || pos+7 > len(s) {
 111  		return "", nil, fmt.Errorf("invalid bech32 separator position")
 112  	}
 113  	hrp := s[:pos]
 114  	dataStr := s[pos+1:]
 115  
 116  	var data []byte
 117  	for _, c := range dataStr {
 118  		idx := strings.IndexRune(bech32Charset, c)
 119  		if idx < 0 {
 120  			return "", nil, fmt.Errorf("invalid bech32 character: %c", c)
 121  		}
 122  		data = append(data, byte(idx))
 123  	}
 124  	if !bech32VerifyChecksum(hrp, data) {
 125  		return "", nil, fmt.Errorf("invalid bech32 checksum")
 126  	}
 127  	return hrp, data[:len(data)-6], nil
 128  }
 129  
 130  func bech32VerifyChecksum(hrp string, data []byte) bool {
 131  	values := append(bech32HRPExpand(hrp), data...)
 132  	return bech32Polymod(values) == 1
 133  }
 134  
 135  func bech32Polymod(values []byte) uint32 {
 136  	gen := [5]uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
 137  	chk := uint32(1)
 138  	for _, v := range values {
 139  		top := chk >> 25
 140  		chk = (chk&0x1ffffff)<<5 ^ uint32(v)
 141  		for i := range 5 {
 142  			if (top>>uint(i))&1 == 1 {
 143  				chk ^= gen[i]
 144  			}
 145  		}
 146  	}
 147  	return chk
 148  }
 149  
 150  func bech32HRPExpand(hrp string) []byte {
 151  	ret := make([]byte, 0, len(hrp)*2+1)
 152  	for _, c := range hrp {
 153  		ret = append(ret, byte(c>>5))
 154  	}
 155  	ret = append(ret, 0)
 156  	for _, c := range hrp {
 157  		ret = append(ret, byte(c&31))
 158  	}
 159  	return ret
 160  }
 161  
 162  func bech32Checksum(hrp string, data []byte) []byte {
 163  	values := append(bech32HRPExpand(hrp), data...)
 164  	values = append(values, 0, 0, 0, 0, 0, 0)
 165  	polymod := bech32Polymod(values) ^ 1
 166  	chk := make([]byte, 6)
 167  	for i := range 6 {
 168  		chk[i] = byte((polymod >> uint(5*(5-i))) & 31)
 169  	}
 170  	return chk
 171  }
 172  
 173  func convertBits(data []byte, fromBits, toBits uint, pad bool) ([]byte, error) {
 174  	acc := uint32(0)
 175  	bits := uint(0)
 176  	maxv := uint32((1 << toBits) - 1)
 177  	var ret []byte
 178  
 179  	for _, d := range data {
 180  		acc = (acc << fromBits) | uint32(d)
 181  		bits += fromBits
 182  		for bits >= toBits {
 183  			bits -= toBits
 184  			ret = append(ret, byte((acc>>bits)&maxv))
 185  		}
 186  	}
 187  
 188  	if pad {
 189  		if bits > 0 {
 190  			ret = append(ret, byte((acc<<(toBits-bits))&maxv))
 191  		}
 192  	} else if bits >= fromBits {
 193  		return nil, fmt.Errorf("excess padding")
 194  	} else if (acc<<(toBits-bits))&maxv != 0 {
 195  		return nil, fmt.Errorf("non-zero padding")
 196  	}
 197  
 198  	return ret, nil
 199  }
 200