nwc.mx raw

   1  package main
   2  
   3  import (
   4  	"git.smesh.lol/smesh/web/common/crypto/nip44"
   5  	"git.smesh.lol/smesh/web/common/helpers"
   6  	"git.smesh.lol/smesh/web/common/jsbridge/ext"
   7  	"git.smesh.lol/smesh/web/common/jsbridge/schnorr"
   8  	"git.smesh.lol/smesh/web/common/jsbridge/subtle"
   9  	"git.smesh.lol/smesh/web/common/nostr"
  10  	nosig "git.smesh.lol/smesh/web/common/nostr/sig"
  11  )
  12  
  13  // NWC: Nostr Wallet Connect (NIP-47) connection management.
  14  // Connections stored in vault storage alongside identities.
  15  // Each connection has its own keypair (walletPubkey + ephemeral secret).
  16  
  17  type nwcConn struct {
  18  	Alias      string
  19  	WalletPK   string // wallet service pubkey
  20  	RelayURL   string
  21  	Secret     string // our NIP-47 ephemeral secret, hex
  22  }
  23  
  24  var walletConns []nwcConn
  25  
  26  const nwcStorageKey = "smesh-nwc"
  27  
  28  func loadNWC() {
  29  	kNwc := string(append([]byte(nil), 's', 'm', 'e', 's', 'h', '-', 'n', 'w', 'c'))
  30  	ext.StorageGet(kNwc, func(data string) {
  31  		if data != "" {
  32  			parseNWCConns(data)
  33  		}
  34  	})
  35  }
  36  
  37  func parseNWCConns(s string) {
  38  	walletConns = nil
  39  	i := 0
  40  	for i < len(s) && s[i] != '[' {
  41  		i++
  42  	}
  43  	i++
  44  	for i < len(s) {
  45  		for i < len(s) && s[i] != '{' && s[i] != ']' {
  46  			i++
  47  		}
  48  		if i >= len(s) || s[i] == ']' {
  49  			break
  50  		}
  51  		end := i + 1
  52  		depth := 1
  53  		for end < len(s) && depth > 0 {
  54  			if s[end] == '{' {
  55  				depth++
  56  			} else if s[end] == '}' {
  57  				depth--
  58  			} else if s[end] == '"' {
  59  				end++
  60  				for end < len(s) && s[end] != '"' {
  61  					if s[end] == '\\' {
  62  						end++
  63  					}
  64  					end++
  65  				}
  66  			}
  67  			end++
  68  		}
  69  		obj := s[i:end]
  70  		kAlias := string(append([]byte(nil), 'a', 'l', 'i', 'a', 's'))
  71  		kWalletPK := string(append([]byte(nil), 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K'))
  72  		kRelay := string(append([]byte(nil), 'r', 'e', 'l', 'a', 'y'))
  73  		kSecret := string(append([]byte(nil), 's', 'e', 'c', 'r', 'e', 't'))
  74  		walletConns = append(walletConns, nwcConn{
  75  			Alias:    helpers.JsonGetString(obj, kAlias),
  76  			WalletPK: helpers.JsonGetString(obj, kWalletPK),
  77  			RelayURL: helpers.JsonGetString(obj, kRelay),
  78  			Secret:   helpers.JsonGetString(obj, kSecret),
  79  		})
  80  		i = end
  81  	}
  82  }
  83  
  84  func saveNWC() {
  85  	b := append([]byte(nil), '[')
  86  	for i, c := range walletConns {
  87  		if i > 0 {
  88  			b = append(b, ',')
  89  		}
  90  		b = append(b, '{', '"', 'a', 'l', 'i', 'a', 's', '"', ':')
  91  		b = b | helpers.JsonString(c.Alias)
  92  		b = append(b, ',', '"', 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K', '"', ':')
  93  		b = b | helpers.JsonString(c.WalletPK)
  94  		b = append(b, ',', '"', 'r', 'e', 'l', 'a', 'y', '"', ':')
  95  		b = b | helpers.JsonString(c.RelayURL)
  96  		b = append(b, ',', '"', 's', 'e', 'c', 'r', 'e', 't', '"', ':')
  97  		b = b | helpers.JsonString(c.Secret)
  98  		b = append(b, '}')
  99  	}
 100  	b = append(b, ']')
 101  	kNwc := string(append([]byte(nil), 's', 'm', 'e', 's', 'h', '-', 'n', 'w', 'c'))
 102  	ext.StorageSet(kNwc, string(b))
 103  }
 104  
 105  func nwcList() (s string) {
 106  	b := append([]byte(nil), '{', '"', 'r', 'e', 's', 'u', 'l', 't', '"', ':', '[')
 107  	for i, c := range walletConns {
 108  		if i > 0 {
 109  			b = append(b, ',')
 110  		}
 111  		b = append(b, '{', '"', 'a', 'l', 'i', 'a', 's', '"', ':')
 112  		b = b | helpers.JsonString(c.Alias)
 113  		b = append(b, ',', '"', 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K', '"', ':')
 114  		b = b | helpers.JsonString(c.WalletPK)
 115  		b = append(b, ',', '"', 'r', 'e', 'l', 'a', 'y', '"', ':')
 116  		b = b | helpers.JsonString(c.RelayURL)
 117  		b = append(b, '}')
 118  	}
 119  	return string(append(b, ']', '}'))
 120  }
 121  
 122  func nwcAdd(paramsJSON string) (s string) {
 123  	kURI := string(append([]byte(nil), 'u', 'r', 'i'))
 124  	kAlias := string(append([]byte(nil), 'a', 'l', 'i', 'a', 's'))
 125  	uri := helpers.JsonGetString(paramsJSON, kURI)
 126  	alias := helpers.JsonGetString(paramsJSON, kAlias)
 127  	if uri == "" {
 128  		return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'u', 'r', 'i')))
 129  	}
 130  	walletPK, relay, secret := parseNWCURI(uri)
 131  	if walletPK == "" || relay == "" || secret == "" {
 132  		return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'u', 'r', 'i')))
 133  	}
 134  	walletConns = append(walletConns, nwcConn{Alias: alias, WalletPK: walletPK, RelayURL: relay, Secret: secret})
 135  	saveNWC()
 136  	return jsonTrue()
 137  }
 138  
 139  func nwcRemove(paramsJSON string) (s string) {
 140  	kWalletPK := string(append([]byte(nil), 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K'))
 141  	walletPK := helpers.JsonGetString(paramsJSON, kWalletPK)
 142  	for i, c := range walletConns {
 143  		if c.WalletPK == walletPK {
 144  			walletConns = walletConns[:i] | walletConns[i+1:]
 145  			saveNWC()
 146  			return jsonTrue()
 147  		}
 148  	}
 149  	return jsonFalse()
 150  }
 151  
 152  func nwcBuildRequest(paramsJSON string) (s string) {
 153  	kWalletPKB := string(append([]byte(nil), 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K'))
 154  	kMethod := string(append([]byte(nil), 'm', 'e', 't', 'h', 'o', 'd'))
 155  	kParams := string(append([]byte(nil), 'p', 'a', 'r', 'a', 'm', 's'))
 156  	walletPK := helpers.JsonGetString(paramsJSON, kWalletPKB)
 157  	method := helpers.JsonGetString(paramsJSON, kMethod)
 158  	reqParams := helpers.JsonGetValue(paramsJSON, kParams)
 159  	if walletPK == "" || method == "" {
 160  		return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 'r', 'a', 'm', 's')))
 161  	}
 162  	var conn *nwcConn
 163  	for i := range walletConns {
 164  		if walletConns[i].WalletPK == walletPK {
 165  			conn = &walletConns[i]
 166  			break
 167  		}
 168  	}
 169  	if conn == nil {
 170  		return jsonErr(string(append([]byte(nil), 'c', 'o', 'n', 'n', 'e', 'c', 't', 'i', 'o', 'n', ' ', 'n', 'o', 't', ' ', 'f', 'o', 'u', 'n', 'd')))
 171  	}
 172  	sk32, ok := helpers.HexDecode32(conn.Secret)
 173  	if !ok {
 174  		return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 's', 'e', 'c', 'r', 'e', 't')))
 175  	}
 176  	pk32, ok := helpers.HexDecode32(walletPK)
 177  	if !ok {
 178  		return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K')))
 179  	}
 180  	convKey, ok := nip44.ConversationKey(sk32, pk32)
 181  	if !ok {
 182  		return jsonErr(string(append([]byte(nil), 'k', 'e', 'y', ' ', 'd', 'e', 'r', 'i', 'v', 'a', 't', 'i', 'o', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
 183  	}
 184  	reqID := randomHex(32)
 185  	reqIDJSON := helpers.JsonString(reqID)
 186  	methodJSON := helpers.JsonString(method)
 187  	bodyB := append([]byte(nil), '{', '"', 'i', 'd', '"', ':')
 188  	bodyB = bodyB | reqIDJSON
 189  	bodyB = append(bodyB, ',', '"', 'm', 'e', 't', 'h', 'o', 'd', '"', ':')
 190  	bodyB = bodyB | methodJSON
 191  	bodyB = append(bodyB, ',', '"', 'p', 'a', 'r', 'a', 'm', 's', '"', ':')
 192  	bodyB = bodyB | reqParams
 193  	bodyB = append(bodyB, '}')
 194  	body := string(bodyB)
 195  	var nonce [32]byte
 196  	subtle.RandomBytes(nonce[:])
 197  	encrypted := nip44.Encrypt(body, convKey, nonce)
 198  	ourPKBytes, ok := schnorr.PubKeyFromSecKey(sk32[:])
 199  	if !ok {
 200  		return jsonErr(string(append([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
 201  	}
 202  	ev := &nostr.Event{
 203  		Kind:      23194,
 204  		Content:   encrypted,
 205  		CreatedAt: 0,
 206  		Tags:      nostr.Tags{nostr.Tag{"p", walletPK}},
 207  		PubKey:    helpers.HexEncode(ourPKBytes),
 208  	}
 209  	(*nosig.Event)(ev).ComputeID()
 210  	idBytes := helpers.HexDecode(ev.ID)
 211  	var aux [32]byte
 212  	subtle.RandomBytes(aux[:])
 213  	sig, ok := schnorr.SignSchnorr(sk32[:], idBytes, aux[:])
 214  	if !ok {
 215  		return jsonErr(string(append([]byte(nil), 's', 'i', 'g', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
 216  	}
 217  	ev.Sig = helpers.HexEncode(sig)
 218  	rb := append([]byte(nil), '{', '"', 'r', 'e', 's', 'u', 'l', 't', '"', ':')
 219  	rb = rb | ev.ToJSON()
 220  	rb = append(rb, ',', '"', 'r', 'e', 'q', 'I', 'D', '"', ':')
 221  	rb = rb | reqIDJSON
 222  	rb = append(rb, '}')
 223  	return string(rb)
 224  }
 225  
 226  func nwcParseResponse(paramsJSON string) (s string) {
 227  	kWalletPKR := string(append([]byte(nil), 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K'))
 228  	kContent := string(append([]byte(nil), 'c', 'o', 'n', 't', 'e', 'n', 't'))
 229  	walletPK := helpers.JsonGetString(paramsJSON, kWalletPKR)
 230  	content := helpers.JsonGetString(paramsJSON, kContent)
 231  	if walletPK == "" || content == "" {
 232  		return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 'r', 'a', 'm', 's')))
 233  	}
 234  	var conn *nwcConn
 235  	for i := range walletConns {
 236  		if walletConns[i].WalletPK == walletPK {
 237  			conn = &walletConns[i]
 238  			break
 239  		}
 240  	}
 241  	if conn == nil {
 242  		return jsonErr(string(append([]byte(nil), 'c', 'o', 'n', 'n', 'e', 'c', 't', 'i', 'o', 'n', ' ', 'n', 'o', 't', ' ', 'f', 'o', 'u', 'n', 'd')))
 243  	}
 244  	sk32, ok := helpers.HexDecode32(conn.Secret)
 245  	if !ok {
 246  		return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 's', 'e', 'c', 'r', 'e', 't')))
 247  	}
 248  	pk32, ok := helpers.HexDecode32(walletPK)
 249  	if !ok {
 250  		return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'w', 'a', 'l', 'l', 'e', 't', 'P', 'K')))
 251  	}
 252  	convKey, ok := nip44.ConversationKey(sk32, pk32)
 253  	if !ok {
 254  		return jsonErr(string(append([]byte(nil), 'k', 'e', 'y', ' ', 'd', 'e', 'r', 'i', 'v', 'a', 't', 'i', 'o', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
 255  	}
 256  	pt, ok := nip44.Decrypt(content, convKey)
 257  	if !ok {
 258  		return jsonErr(string(append([]byte(nil), 'd', 'e', 'c', 'r', 'y', 'p', 't', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
 259  	}
 260  	return jsonResult(pt)
 261  }
 262  
 263  // parseNWCURI parses nostr+walletconnect://<walletPK>?relay=<url>&secret=<hex>
 264  func parseNWCURI(uri string) (walletPK, relay, secret string) {
 265  	scheme := string(append([]byte(nil), 'n', 'o', 's', 't', 'r', '+', 'w', 'a', 'l', 'l', 'e', 't', 'c', 'o', 'n', 'n', 'e', 'c', 't', ':', '/', '/'))
 266  	if len(uri) < len(scheme) || uri[:len(scheme)] != scheme {
 267  		return "", "", ""
 268  	}
 269  	rest := uri[len(scheme):]
 270  	// Find the '?' separating walletPK from query params
 271  	qIdx := -1
 272  	for i := 0; i < len(rest); i++ {
 273  		if rest[i] == '?' {
 274  			qIdx = i
 275  			break
 276  		}
 277  	}
 278  	if qIdx < 0 {
 279  		return "", "", ""
 280  	}
 281  	walletPK = rest[:qIdx]
 282  	query := rest[qIdx+1:]
 283  	// Parse key=value pairs separated by '&'
 284  	kRelay := string(append([]byte(nil), 'r', 'e', 'l', 'a', 'y'))
 285  	kSecret := string(append([]byte(nil), 's', 'e', 'c', 'r', 'e', 't'))
 286  	start := 0
 287  	for start <= len(query) {
 288  		end := start
 289  		for end < len(query) && query[end] != '&' {
 290  			end++
 291  		}
 292  		pair := query[start:end]
 293  		eqIdx := -1
 294  		for j := 0; j < len(pair); j++ {
 295  			if pair[j] == '=' {
 296  				eqIdx = j
 297  				break
 298  			}
 299  		}
 300  		if eqIdx > 0 {
 301  			k := pair[:eqIdx]
 302  			v := pair[eqIdx+1:]
 303  			if k == kRelay {
 304  				relay = v
 305  			} else if k == kSecret {
 306  				secret = v
 307  			}
 308  		}
 309  		start = end + 1
 310  	}
 311  	if walletPK == "" || relay == "" || secret == "" {
 312  		return "", "", ""
 313  	}
 314  	return walletPK, relay, secret
 315  }
 316  
 317  func randomHex(n int32) (s string) {
 318  	b := []byte{:n}
 319  	subtle.RandomBytes(b)
 320  	return helpers.HexEncode(b)
 321  }
 322  
 323