nrc.go raw
1 //go:build !(js && wasm)
2
3 package database
4
5 import (
6 "crypto/rand"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "time"
11
12 "github.com/dgraph-io/badger/v4"
13 "next.orly.dev/pkg/lol/chk"
14 "next.orly.dev/pkg/lol/log"
15
16 "next.orly.dev/pkg/nostr/crypto/keys"
17 "next.orly.dev/pkg/nostr/encoders/hex"
18 )
19
20 // Key prefixes for NRC data
21 const (
22 nrcConnectionPrefix = "nrc:conn:" // NRC connections by ID
23 nrcDerivedPubkeyPrefix = "nrc:pubkey:" // Index: derived pubkey -> connection ID
24 )
25
26 // NRCConnection stores an NRC connection configuration in the database.
27 type NRCConnection struct {
28 ID string `json:"id"` // Unique identifier (hex of first 8 bytes of secret)
29 Label string `json:"label"` // Human-readable label (e.g., "Phone", "Laptop")
30 Secret []byte `json:"secret"` // 32-byte secret for client authentication
31 DerivedPubkey []byte `json:"derived_pubkey"` // Pubkey derived from secret (for efficient lookups)
32 RendezvousURL string `json:"rendezvous_url"` // WebSocket URL of the rendezvous relay
33 CreatedAt int64 `json:"created_at"` // Unix timestamp
34 LastUsed int64 `json:"last_used"` // Unix timestamp of last connection (0 if never)
35 CreatedBy []byte `json:"created_by"` // Pubkey of admin who created this connection
36 }
37
38 // GetNRCConnection retrieves an NRC connection by ID.
39 func (d *D) GetNRCConnection(id string) (conn *NRCConnection, err error) {
40 key := []byte(nrcConnectionPrefix + id)
41
42 err = d.DB.View(func(txn *badger.Txn) error {
43 item, err := txn.Get(key)
44 if errors.Is(err, badger.ErrKeyNotFound) {
45 return err
46 }
47 if err != nil {
48 return err
49 }
50 return item.Value(func(val []byte) error {
51 conn = &NRCConnection{}
52 return json.Unmarshal(val, conn)
53 })
54 })
55 return
56 }
57
58 // SaveNRCConnection stores an NRC connection in the database.
59 func (d *D) SaveNRCConnection(conn *NRCConnection) error {
60 data, err := json.Marshal(conn)
61 if err != nil {
62 return fmt.Errorf("failed to marshal connection: %w", err)
63 }
64
65 key := []byte(nrcConnectionPrefix + conn.ID)
66
67 return d.DB.Update(func(txn *badger.Txn) error {
68 // Save the connection
69 if err := txn.Set(key, data); err != nil {
70 return err
71 }
72 // Save the derived pubkey index (pubkey -> connection ID)
73 if len(conn.DerivedPubkey) > 0 {
74 indexKey := append([]byte(nrcDerivedPubkeyPrefix), conn.DerivedPubkey...)
75 if err := txn.Set(indexKey, []byte(conn.ID)); err != nil {
76 return err
77 }
78 }
79 return nil
80 })
81 }
82
83 // GetNRCConnectionByDerivedPubkey retrieves an NRC connection by its derived pubkey.
84 func (d *D) GetNRCConnectionByDerivedPubkey(derivedPubkey []byte) (*NRCConnection, error) {
85 if len(derivedPubkey) == 0 {
86 return nil, fmt.Errorf("derived pubkey is required")
87 }
88
89 var connID string
90 indexKey := append([]byte(nrcDerivedPubkeyPrefix), derivedPubkey...)
91
92 err := d.DB.View(func(txn *badger.Txn) error {
93 item, err := txn.Get(indexKey)
94 if err != nil {
95 return err
96 }
97 return item.Value(func(val []byte) error {
98 connID = string(val)
99 return nil
100 })
101 })
102 if err != nil {
103 return nil, err
104 }
105
106 return d.GetNRCConnection(connID)
107 }
108
109 // DeleteNRCConnection removes an NRC connection from the database.
110 func (d *D) DeleteNRCConnection(id string) error {
111 // First get the connection to find its derived pubkey for index cleanup
112 conn, err := d.GetNRCConnection(id)
113 if err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
114 return err
115 }
116
117 key := []byte(nrcConnectionPrefix + id)
118
119 return d.DB.Update(func(txn *badger.Txn) error {
120 // Delete the connection
121 if err := txn.Delete(key); err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
122 return err
123 }
124 // Delete the derived pubkey index if we have the connection
125 if conn != nil && len(conn.DerivedPubkey) > 0 {
126 indexKey := append([]byte(nrcDerivedPubkeyPrefix), conn.DerivedPubkey...)
127 if err := txn.Delete(indexKey); err != nil && !errors.Is(err, badger.ErrKeyNotFound) {
128 return err
129 }
130 }
131 return nil
132 })
133 }
134
135 // GetAllNRCConnections returns all NRC connections.
136 func (d *D) GetAllNRCConnections() (conns []*NRCConnection, err error) {
137 prefix := []byte(nrcConnectionPrefix)
138
139 err = d.DB.View(func(txn *badger.Txn) error {
140 opts := badger.DefaultIteratorOptions
141 opts.Prefix = prefix
142 it := txn.NewIterator(opts)
143 defer it.Close()
144
145 for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
146 item := it.Item()
147 err := item.Value(func(val []byte) error {
148 conn := &NRCConnection{}
149 if err := json.Unmarshal(val, conn); err != nil {
150 return err
151 }
152 conns = append(conns, conn)
153 return nil
154 })
155 if err != nil {
156 return err
157 }
158 }
159 return nil
160 })
161 return
162 }
163
164 // CreateNRCConnection generates a new NRC connection with a random secret.
165 // createdBy is the pubkey of the admin creating this connection (can be nil for system-created).
166 func (d *D) CreateNRCConnection(label string, createdBy []byte) (*NRCConnection, error) {
167 // Generate random 32-byte secret
168 secret := make([]byte, 32)
169 if _, err := rand.Read(secret); err != nil {
170 return nil, fmt.Errorf("failed to generate random secret: %w", err)
171 }
172
173 // Derive pubkey from secret
174 derivedPubkey, err := keys.SecretBytesToPubKeyBytes(secret)
175 if err != nil {
176 return nil, fmt.Errorf("failed to derive pubkey from secret: %w", err)
177 }
178
179 // Use first 8 bytes of secret as ID (hex encoded = 16 chars)
180 id := string(hex.Enc(secret[:8]))
181
182 conn := &NRCConnection{
183 ID: id,
184 Label: label,
185 Secret: secret,
186 DerivedPubkey: derivedPubkey,
187 CreatedAt: time.Now().Unix(),
188 LastUsed: 0,
189 CreatedBy: createdBy,
190 }
191
192 if err := d.SaveNRCConnection(conn); chk.E(err) {
193 return nil, err
194 }
195
196 log.I.F("created NRC connection: id=%s label=%s", id, label)
197 return conn, nil
198 }
199
200 // GetNRCConnectionURI generates the full connection URI for a connection.
201 // relayPubkey is the relay's public key (32 bytes).
202 // rendezvousURL is the public relay URL.
203 func (d *D) GetNRCConnectionURI(conn *NRCConnection, relayPubkey []byte, rendezvousURL string) (string, error) {
204 if len(relayPubkey) != 32 {
205 return "", fmt.Errorf("invalid relay pubkey length: %d", len(relayPubkey))
206 }
207 if rendezvousURL == "" {
208 return "", fmt.Errorf("rendezvous URL is required")
209 }
210
211 relayPubkeyHex := hex.Enc(relayPubkey)
212 secretHex := hex.Enc(conn.Secret)
213
214 // Secret-only URI
215 uri := fmt.Sprintf("nostr+relayconnect://%s?relay=%s&secret=%s",
216 relayPubkeyHex, rendezvousURL, secretHex)
217
218 if conn.Label != "" {
219 uri += fmt.Sprintf("&name=%s", conn.Label)
220 }
221
222 return uri, nil
223 }
224
225 // GetNRCAuthorizedSecrets returns a map of derived pubkeys to labels for all connections.
226 // This is used by the NRC bridge to authorize incoming connections.
227 func (d *D) GetNRCAuthorizedSecrets() (map[string]string, error) {
228 conns, err := d.GetAllNRCConnections()
229 if err != nil {
230 return nil, err
231 }
232
233 result := make(map[string]string)
234 for _, conn := range conns {
235 // Derive pubkey from secret
236 pubkey, err := keys.SecretBytesToPubKeyBytes(conn.Secret)
237 if err != nil {
238 log.W.F("failed to derive pubkey for NRC connection %s: %v", conn.ID, err)
239 continue
240 }
241 pubkeyHex := string(hex.Enc(pubkey))
242 result[pubkeyHex] = conn.Label
243 }
244
245 return result, nil
246 }
247
248 // UpdateNRCConnectionLastUsed updates the last used timestamp for a connection.
249 func (d *D) UpdateNRCConnectionLastUsed(id string) error {
250 conn, err := d.GetNRCConnection(id)
251 if err != nil {
252 return err
253 }
254
255 conn.LastUsed = time.Now().Unix()
256 return d.SaveNRCConnection(conn)
257 }
258
259 // NRCAuthorizer wraps D to implement the NRC authorization interface.
260 // This allows the NRC bridge to look up authorized clients from the database.
261 type NRCAuthorizer struct {
262 db *D
263 }
264
265 // NewNRCAuthorizer creates a new NRC authorizer from a database.
266 func NewNRCAuthorizer(db *D) *NRCAuthorizer {
267 return &NRCAuthorizer{db: db}
268 }
269
270 // GetNRCClientByPubkey looks up an authorized client by their derived pubkey.
271 // Returns the client ID, label, and whether the client was found.
272 func (a *NRCAuthorizer) GetNRCClientByPubkey(derivedPubkey []byte) (id string, label string, found bool, err error) {
273 conn, err := a.db.GetNRCConnectionByDerivedPubkey(derivedPubkey)
274 if err != nil {
275 // badger.ErrKeyNotFound means not authorized
276 if errors.Is(err, badger.ErrKeyNotFound) {
277 return "", "", false, nil
278 }
279 return "", "", false, err
280 }
281 if conn == nil {
282 return "", "", false, nil
283 }
284 return conn.ID, conn.Label, true, nil
285 }
286
287 // UpdateNRCClientLastUsed updates the last used timestamp for tracking.
288 func (a *NRCAuthorizer) UpdateNRCClientLastUsed(id string) error {
289 return a.db.UpdateNRCConnectionLastUsed(id)
290 }
291