nwc.mx raw

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