uri.go raw
1 package nrc
2
3 import (
4 "errors"
5 "net/url"
6
7 "next.orly.dev/pkg/nostr/crypto/encryption"
8 "next.orly.dev/pkg/nostr/encoders/hex"
9 "next.orly.dev/pkg/nostr/interfaces/signer"
10 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
11 "next.orly.dev/pkg/lol/chk"
12 )
13
14 // AuthMode defines the authentication mode for NRC connections.
15 type AuthMode int
16
17 const (
18 // AuthModeSecret uses a shared secret for authentication.
19 AuthModeSecret AuthMode = iota
20 )
21
22 // ConnectionURI represents a parsed nostr+relayconnect:// URI.
23 type ConnectionURI struct {
24 // RelayPubkey is the public key of the private relay (32 bytes).
25 RelayPubkey []byte
26 // RendezvousRelay is the WebSocket URL of the public relay.
27 RendezvousRelay string
28 // AuthMode indicates the authentication mode.
29 AuthMode AuthMode
30 // DeviceName is an optional human-readable device identifier.
31 DeviceName string
32
33 // Secret-based authentication fields
34 clientSecretKey signer.I
35 conversationKey []byte
36 }
37
38 // GetClientSigner returns the signer derived from the secret (secret-based auth only).
39 func (c *ConnectionURI) GetClientSigner() signer.I {
40 return c.clientSecretKey
41 }
42
43 // GetConversationKey returns the NIP-44 conversation key (secret-based auth only).
44 func (c *ConnectionURI) GetConversationKey() []byte {
45 return c.conversationKey
46 }
47
48 // ParseConnectionURI parses a nostr+relayconnect:// URI.
49 //
50 // URI format:
51 //
52 // nostr+relayconnect://<relay-pubkey>?relay=<rendezvous-relay>&secret=<client-secret>[&name=<device-name>]
53 func ParseConnectionURI(nrcURI string) (conn *ConnectionURI, err error) {
54 var p *url.URL
55 if p, err = url.Parse(nrcURI); chk.E(err) {
56 return
57 }
58 if p == nil {
59 err = errors.New("invalid uri")
60 return
61 }
62
63 conn = &ConnectionURI{}
64
65 // Validate scheme
66 if p.Scheme != "nostr+relayconnect" {
67 err = errors.New("incorrect scheme: expected nostr+relayconnect")
68 return
69 }
70
71 // Parse relay pubkey from host
72 if conn.RelayPubkey, err = hex.Dec(p.Host); chk.E(err) {
73 err = errors.New("invalid relay public key")
74 return
75 }
76 if len(conn.RelayPubkey) != 32 {
77 err = errors.New("relay public key must be 32 bytes")
78 return
79 }
80
81 query := p.Query()
82
83 // Parse rendezvous relay URL (required)
84 relayParam := query.Get("relay")
85 if relayParam == "" {
86 err = errors.New("missing relay parameter")
87 return
88 }
89 conn.RendezvousRelay = relayParam
90
91 // Parse optional device name
92 conn.DeviceName = query.Get("name")
93
94 conn.AuthMode = AuthModeSecret
95 // Parse secret for secret-based auth
96 secret := query.Get("secret")
97 if secret == "" {
98 err = errors.New("missing secret parameter")
99 return
100 }
101
102 var secretBytes []byte
103 if secretBytes, err = hex.Dec(secret); chk.E(err) {
104 err = errors.New("invalid secret: must be hex-encoded")
105 return
106 }
107 if len(secretBytes) != 32 {
108 err = errors.New("secret must be 32 bytes")
109 return
110 }
111
112 // Create signer from secret
113 var clientKey *p8k.Signer
114 if clientKey, err = p8k.New(); chk.E(err) {
115 return
116 }
117 if err = clientKey.InitSec(secretBytes); chk.E(err) {
118 return
119 }
120 conn.clientSecretKey = clientKey
121
122 // Generate conversation key using NIP-44 key derivation
123 if conn.conversationKey, err = encryption.GenerateConversationKey(
124 clientKey.Sec(),
125 conn.RelayPubkey,
126 ); chk.E(err) {
127 return
128 }
129
130 return
131 }
132
133 // GenerateConnectionURI creates a new NRC connection URI with a random secret.
134 func GenerateConnectionURI(relayPubkey []byte, rendezvousRelay string, deviceName string) (uri string, secret []byte, err error) {
135 if len(relayPubkey) != 32 {
136 err = errors.New("relay public key must be 32 bytes")
137 return
138 }
139
140 // Generate random 32-byte secret
141 var clientKey *p8k.Signer
142 if clientKey, err = p8k.New(); chk.E(err) {
143 return
144 }
145 if err = clientKey.Generate(); chk.E(err) {
146 return
147 }
148 secret = clientKey.Sec()
149
150 // Build URI
151 u := &url.URL{
152 Scheme: "nostr+relayconnect",
153 Host: string(hex.Enc(relayPubkey)),
154 }
155 q := u.Query()
156 q.Set("relay", rendezvousRelay)
157 q.Set("secret", string(hex.Enc(secret)))
158 if deviceName != "" {
159 q.Set("name", deviceName)
160 }
161 u.RawQuery = q.Encode()
162 uri = u.String()
163 return
164 }
165
166