From 581ceecbf046f99b39885c74e2780a5320e5b15e Mon Sep 17 00:00:00 2001 From: bndw Date: Fri, 13 Feb 2026 17:35:32 -0800 Subject: feat: add Nostr protocol implementation (internal/nostr, internal/websocket) --- internal/nostr/bech32.go | 162 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 internal/nostr/bech32.go (limited to 'internal/nostr/bech32.go') diff --git a/internal/nostr/bech32.go b/internal/nostr/bech32.go new file mode 100644 index 0000000..c8b1293 --- /dev/null +++ b/internal/nostr/bech32.go @@ -0,0 +1,162 @@ +package nostr + +import ( + "fmt" + "strings" +) + +// Bech32 encoding/decoding for NIP-19 (npub, nsec, note, etc.) +// Implements BIP-173 bech32 encoding. + +const bech32Alphabet = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + +var bech32AlphabetMap [256]int8 + +func init() { + for i := range bech32AlphabetMap { + bech32AlphabetMap[i] = -1 + } + for i, c := range bech32Alphabet { + bech32AlphabetMap[c] = int8(i) + } +} + +// bech32Polymod computes the BCH checksum. +func bech32Polymod(values []int) int { + gen := []int{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3} + chk := 1 + for _, v := range values { + top := chk >> 25 + chk = (chk&0x1ffffff)<<5 ^ v + for i := 0; i < 5; i++ { + if (top>>i)&1 == 1 { + chk ^= gen[i] + } + } + } + return chk +} + +// bech32HRPExpand expands the human-readable part for checksum computation. +func bech32HRPExpand(hrp string) []int { + result := make([]int, len(hrp)*2+1) + for i, c := range hrp { + result[i] = int(c) >> 5 + result[i+len(hrp)+1] = int(c) & 31 + } + return result +} + +// bech32CreateChecksum creates the 6-character checksum. +func bech32CreateChecksum(hrp string, data []int) []int { + values := append(bech32HRPExpand(hrp), data...) + values = append(values, []int{0, 0, 0, 0, 0, 0}...) + polymod := bech32Polymod(values) ^ 1 + checksum := make([]int, 6) + for i := 0; i < 6; i++ { + checksum[i] = (polymod >> (5 * (5 - i))) & 31 + } + return checksum +} + +// bech32VerifyChecksum verifies the checksum of bech32 data. +func bech32VerifyChecksum(hrp string, data []int) bool { + return bech32Polymod(append(bech32HRPExpand(hrp), data...)) == 1 +} + +// convertBits converts between bit groups. +func convertBits(data []byte, fromBits, toBits int, pad bool) ([]int, error) { + acc := 0 + bits := 0 + result := make([]int, 0, len(data)*fromBits/toBits+1) + maxv := (1 << toBits) - 1 + + for _, value := range data { + acc = (acc << fromBits) | int(value) + bits += fromBits + for bits >= toBits { + bits -= toBits + result = append(result, (acc>>bits)&maxv) + } + } + + if pad { + if bits > 0 { + result = append(result, (acc<<(toBits-bits))&maxv) + } + } else if bits >= fromBits || ((acc<<(toBits-bits))&maxv) != 0 { + return nil, fmt.Errorf("invalid padding") + } + + return result, nil +} + +// Bech32Encode encodes data with the given human-readable prefix. +func Bech32Encode(hrp string, data []byte) (string, error) { + values, err := convertBits(data, 8, 5, true) + if err != nil { + return "", err + } + + checksum := bech32CreateChecksum(hrp, values) + combined := append(values, checksum...) + + var result strings.Builder + result.WriteString(hrp) + result.WriteByte('1') + for _, v := range combined { + result.WriteByte(bech32Alphabet[v]) + } + + return result.String(), nil +} + +// Bech32Decode decodes a bech32 string, returning the HRP and data. +func Bech32Decode(s string) (string, []byte, error) { + s = strings.ToLower(s) + + pos := strings.LastIndexByte(s, '1') + if pos < 1 || pos+7 > len(s) { + return "", nil, fmt.Errorf("invalid bech32 string") + } + + hrp := s[:pos] + dataStr := s[pos+1:] + + data := make([]int, len(dataStr)) + for i, c := range dataStr { + val := bech32AlphabetMap[c] + if val == -1 { + return "", nil, fmt.Errorf("invalid character: %c", c) + } + data[i] = int(val) + } + + if !bech32VerifyChecksum(hrp, data) { + return "", nil, fmt.Errorf("invalid checksum") + } + + // Remove checksum + data = data[:len(data)-6] + + // Convert from 5-bit to 8-bit + result, err := convertBits(intSliceToBytes(data), 5, 8, false) + if err != nil { + return "", nil, err + } + + bytes := make([]byte, len(result)) + for i, v := range result { + bytes[i] = byte(v) + } + + return hrp, bytes, nil +} + +func intSliceToBytes(data []int) []byte { + result := make([]byte, len(data)) + for i, v := range data { + result[i] = byte(v) + } + return result +} -- cgit v1.2.3