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