config.go raw

   1  package config
   2  
   3  import (
   4  	"crypto/rand"
   5  	"crypto/sha256"
   6  	"encoding/hex"
   7  	"errors"
   8  	"fmt"
   9  	"os"
  10  	"strings"
  11  	"sync"
  12  
  13  	"github.com/getAlby/go-nostr/nip19"
  14  	"github.com/getAlby/hub/constants"
  15  	"github.com/getAlby/hub/db"
  16  	"github.com/getAlby/hub/logger"
  17  	"github.com/sirupsen/logrus"
  18  	"gorm.io/gorm"
  19  	"gorm.io/gorm/clause"
  20  )
  21  
  22  type config struct {
  23  	Env                *AppConfig
  24  	db                 *gorm.DB
  25  	cache              map[string]map[string]string // key -> encryptionKeyHash -> value
  26  	cacheMutex         sync.Mutex
  27  	jwtSecret          string
  28  	jwtSecretMutex     sync.Mutex
  29  	sessionSecret      string
  30  	sessionSecretMutex sync.Mutex
  31  }
  32  
  33  const (
  34  	unlockPasswordCheck = "THIS STRING SHOULD MATCH IF PASSWORD IS CORRECT"
  35  )
  36  
  37  func NewConfig(env *AppConfig, db *gorm.DB) (*config, error) {
  38  	cfg := &config{
  39  		db:    db,
  40  		cache: map[string]map[string]string{},
  41  	}
  42  	err := cfg.init(env)
  43  	if err != nil {
  44  		return nil, err
  45  	}
  46  
  47  	return cfg, nil
  48  }
  49  
  50  func (cfg *config) init(env *AppConfig) error {
  51  	cfg.Env = env
  52  
  53  	// Only set Relay from env if not already configured in DB
  54  	if cfg.Env.Relay != "" {
  55  		existingRelay, _ := cfg.Get("Relay", "")
  56  		if existingRelay == "" {
  57  			err := cfg.SetUpdate("Relay", cfg.Env.Relay, "")
  58  			if err != nil {
  59  				return err
  60  			}
  61  		}
  62  	}
  63  	if cfg.Env.LNBackendType != "" {
  64  		err := cfg.SetIgnore("LNBackendType", cfg.Env.LNBackendType, "")
  65  		if err != nil {
  66  			return err
  67  		}
  68  	}
  69  
  70  	// LND specific to support env variables
  71  	if cfg.Env.LNDAddress != "" {
  72  		err := cfg.SetUpdate("LNDAddress", cfg.Env.LNDAddress, "")
  73  		if err != nil {
  74  			return err
  75  		}
  76  	}
  77  	if cfg.Env.LNDCertFile != "" {
  78  		certBytes, err := os.ReadFile(cfg.Env.LNDCertFile)
  79  		if err != nil {
  80  			logger.Logger.WithError(err).Error("Failed to read LND cert file")
  81  			return err
  82  		}
  83  		certHex := hex.EncodeToString(certBytes)
  84  		err = cfg.SetUpdate("LNDCertHex", certHex, "")
  85  		if err != nil {
  86  			return err
  87  		}
  88  	} else if cfg.Env.LNBackendType == "LND" {
  89  		// If no LNDCertFile is provided, clear any stored certificate
  90  		// hex value so that no certificate is used for TLS verification.
  91  		err := cfg.SetUpdate("LNDCertHex", "", "")
  92  		if err != nil {
  93  			return err
  94  		}
  95  	}
  96  	if cfg.Env.LNDMacaroonFile != "" {
  97  		macBytes, err := os.ReadFile(cfg.Env.LNDMacaroonFile)
  98  		if err != nil {
  99  			logger.Logger.WithError(err).Error("Failed to read LND macaroon file")
 100  			return err
 101  		}
 102  		macHex := hex.EncodeToString(macBytes)
 103  		err = cfg.SetUpdate("LNDMacaroonHex", macHex, "")
 104  		if err != nil {
 105  			return err
 106  		}
 107  	}
 108  	// Phoenix specific to support env variables
 109  	if cfg.Env.PhoenixdAddress != "" {
 110  		err := cfg.SetIgnore("PhoenixdAddress", cfg.Env.PhoenixdAddress, "")
 111  		if err != nil {
 112  			return err
 113  		}
 114  	}
 115  	if cfg.Env.PhoenixdAuthorization != "" {
 116  		err := cfg.SetIgnore("PhoenixdAuthorization", cfg.Env.PhoenixdAuthorization, "")
 117  		if err != nil {
 118  			return err
 119  		}
 120  	}
 121  
 122  	// CLN specific to support env variables
 123  	if cfg.Env.CLNAddress != "" {
 124  		err := cfg.SetUpdate("CLNAddress", cfg.Env.CLNAddress, "")
 125  		if err != nil {
 126  			return err
 127  		}
 128  	}
 129  	if cfg.Env.CLNLightningDir != "" {
 130  		err := cfg.SetUpdate("CLNLightningDir", cfg.Env.CLNLightningDir, "")
 131  		if err != nil {
 132  			return err
 133  		}
 134  	}
 135  	if cfg.Env.CLNAddressHold != "" {
 136  		err := cfg.SetUpdate("CLNAddressHold", cfg.Env.CLNAddressHold, "")
 137  		if err != nil {
 138  			return err
 139  		}
 140  	}
 141  
 142  	return nil
 143  }
 144  
 145  func (cfg *config) SetupCompleted() (bool, error) {
 146  	nodeLastStartTime, err := cfg.Get("NodeLastStartTime", "")
 147  	if err != nil {
 148  		return false, err
 149  	}
 150  
 151  	logger.Logger.WithFields(logrus.Fields{
 152  		"has_node_last_start_time": nodeLastStartTime != "",
 153  	}).Debug("Checking if setup is completed")
 154  	return nodeLastStartTime != "", nil
 155  }
 156  
 157  func (cfg *config) GetJWTSecret() (string, error) {
 158  	cfg.jwtSecretMutex.Lock()
 159  	jwtSecret := cfg.jwtSecret
 160  	cfg.jwtSecretMutex.Unlock()
 161  
 162  	if jwtSecret == "" {
 163  		return "", errors.New("config not unlocked")
 164  	}
 165  
 166  	return jwtSecret, nil
 167  }
 168  
 169  // Decrypt and store the JWT secret in memory
 170  func (cfg *config) LoadJWTSecret(encryptionKey string) error {
 171  	if !cfg.CheckUnlockPassword(encryptionKey) {
 172  		return errors.New("incorrect password")
 173  	}
 174  
 175  	cfg.jwtSecretMutex.Lock()
 176  	if cfg.jwtSecret != "" {
 177  		cfg.jwtSecretMutex.Unlock()
 178  		return nil
 179  	}
 180  	cfg.jwtSecretMutex.Unlock()
 181  
 182  	// TODO: remove encryptedJwtSecret check after 2027-01-01
 183  	// - all hubs should have updated to use an encrypted JWT secret by then
 184  	encryptedJwtSecret, err := cfg.Get("JWTSecret", "")
 185  	if err != nil {
 186  		return err
 187  	}
 188  	jwtSecret, err := cfg.Get("JWTSecret", encryptionKey)
 189  	if err != nil {
 190  		return err
 191  	}
 192  	// generate a new one if none exists yet OR if the user has an unencrypted secret
 193  	if jwtSecret == "" || jwtSecret == encryptedJwtSecret {
 194  		hexSecret, err := randomHex(32)
 195  		if err != nil {
 196  			logger.Logger.WithError(err).Error("failed to generate JWT secret")
 197  			return err
 198  		}
 199  		jwtSecret = hexSecret
 200  		logger.Logger.Info("Generated new JWT secret")
 201  
 202  		err = cfg.SetUpdate("JWTSecret", jwtSecret, encryptionKey)
 203  		if err != nil {
 204  			logger.Logger.WithError(err).Error("failed to save JWT secret")
 205  			return err
 206  		}
 207  	}
 208  	cfg.jwtSecretMutex.Lock()
 209  	cfg.jwtSecret = jwtSecret
 210  	cfg.jwtSecretMutex.Unlock()
 211  	return nil
 212  }
 213  
 214  func (cfg *config) GetRelayUrls() []string {
 215  	relayUrls, _ := cfg.Get("Relay", "")
 216  	return strings.Split(relayUrls, ",")
 217  }
 218  
 219  func (cfg *config) GetNetwork() string {
 220  	env := cfg.GetEnv()
 221  
 222  	if env.Network != "" {
 223  		return env.Network
 224  	}
 225  
 226  	if env.LDKNetwork != "" {
 227  		return env.LDKNetwork
 228  	}
 229  
 230  	return "bitcoin"
 231  }
 232  
 233  func (cfg *config) GetMempoolUrl() string {
 234  	mempoolApiUrl := cfg.GetEnv().MempoolApi
 235  	return strings.TrimSuffix(mempoolApiUrl, "/api")
 236  }
 237  
 238  func (cfg *config) getEncryptionKeyHash(encryptionKey string) string {
 239  	if encryptionKey == "" {
 240  		return ""
 241  	}
 242  	hash := sha256.Sum256([]byte(encryptionKey))
 243  	// For cache key purposes, 8 bytes (16 hex chars) provides:
 244  	//   2^64 possible values = ~18 quintillion combinations
 245  	//   More than sufficient to avoid collisions for cache keys
 246  	return hex.EncodeToString(hash[:8])
 247  }
 248  
 249  func (cfg *config) Get(key string, encryptionKey string) (string, error) {
 250  	cfg.cacheMutex.Lock()
 251  	defer cfg.cacheMutex.Unlock()
 252  
 253  	encKeyHash := cfg.getEncryptionKeyHash(encryptionKey)
 254  
 255  	if keyCache, ok := cfg.cache[key]; ok {
 256  		if cachedValue, ok := keyCache[encKeyHash]; ok {
 257  			logger.Logger.WithField("key", key).Debug("hit config cache")
 258  			return cachedValue, nil
 259  		}
 260  	}
 261  	logger.Logger.WithField("key", key).Debug("missed config cache")
 262  
 263  	value, err := cfg.get(key, encryptionKey, cfg.db)
 264  	if err != nil {
 265  		return "", err
 266  	}
 267  
 268  	if cfg.cache[key] == nil {
 269  		cfg.cache[key] = make(map[string]string)
 270  	}
 271  	cfg.cache[key][encKeyHash] = value
 272  	logger.Logger.WithField("key", key).Debug("set config cache")
 273  	return value, nil
 274  }
 275  
 276  func (cfg *config) get(key string, encryptionKey string, gormDB *gorm.DB) (string, error) {
 277  	var userConfig db.UserConfig
 278  	err := gormDB.Where(&db.UserConfig{Key: key}).Limit(1).Find(&userConfig).Error
 279  	if err != nil {
 280  		return "", fmt.Errorf("failed to get configuration value: %w", gormDB.Error)
 281  	}
 282  
 283  	value := userConfig.Value
 284  	if userConfig.Value != "" && encryptionKey != "" && userConfig.Encrypted {
 285  		decrypted, err := AesGcmDecryptWithPassword(value, encryptionKey)
 286  		if err != nil {
 287  			return "", err
 288  		}
 289  		value = decrypted
 290  	}
 291  	return value, nil
 292  }
 293  
 294  func (cfg *config) set(key string, value string, clauses clause.OnConflict, encryptionKey string, gormDB *gorm.DB) error {
 295  	if encryptionKey != "" {
 296  		encrypted, err := AesGcmEncryptWithPassword(value, encryptionKey)
 297  		if err != nil {
 298  			return fmt.Errorf("failed to encrypt: %v", err)
 299  		}
 300  		value = encrypted
 301  	}
 302  	userConfig := db.UserConfig{Key: key, Value: value, Encrypted: encryptionKey != ""}
 303  	result := gormDB.Clauses(clauses).Create(&userConfig)
 304  
 305  	if result.Error != nil {
 306  		return fmt.Errorf("failed to save key to config: %v", result.Error)
 307  	}
 308  
 309  	logger.Logger.WithField("key", key).Debug("clearing config cache")
 310  	cfg.cacheMutex.Lock()
 311  	defer cfg.cacheMutex.Unlock()
 312  	delete(cfg.cache, key)
 313  
 314  	return nil
 315  }
 316  
 317  func (cfg *config) SetIgnore(key string, value string, encryptionKey string) error {
 318  	clauses := clause.OnConflict{
 319  		Columns:   []clause.Column{{Name: "key"}},
 320  		DoNothing: true,
 321  	}
 322  	err := cfg.set(key, value, clauses, encryptionKey, cfg.db)
 323  	if err != nil {
 324  		logger.Logger.WithField("key", key).WithError(err).Error("Failed to set config key with ignore", err)
 325  		return err
 326  	}
 327  	return nil
 328  }
 329  
 330  func (cfg *config) SetUpdate(key string, value string, encryptionKey string) error {
 331  	clauses := clause.OnConflict{
 332  		Columns:   []clause.Column{{Name: "key"}},
 333  		DoUpdates: clause.AssignmentColumns([]string{"value", "encrypted"}),
 334  	}
 335  	err := cfg.set(key, value, clauses, encryptionKey, cfg.db)
 336  	if err != nil {
 337  		logger.Logger.WithField("key", key).WithError(err).Error("Failed to set config key with update", err)
 338  		return err
 339  	}
 340  	return nil
 341  }
 342  
 343  func (cfg *config) ChangeUnlockPassword(currentUnlockPassword string, newUnlockPassword string) error {
 344  	if newUnlockPassword == "" {
 345  		return errors.New("new unlock password must not be empty")
 346  	}
 347  	if !cfg.CheckUnlockPassword(currentUnlockPassword) {
 348  		return errors.New("incorrect password")
 349  	}
 350  	err := cfg.db.Transaction(func(tx *gorm.DB) error {
 351  
 352  		var encryptedUserConfigs []db.UserConfig
 353  		err := tx.Where(&db.UserConfig{Encrypted: true}).Find(&encryptedUserConfigs).Error
 354  		if err != nil {
 355  			return err
 356  		}
 357  
 358  		logger.Logger.WithField("count", len(encryptedUserConfigs)).Info("Updating encrypted entries")
 359  
 360  		for _, userConfig := range encryptedUserConfigs {
 361  			decryptedValue, err := cfg.get(userConfig.Key, currentUnlockPassword, tx)
 362  			if err != nil {
 363  				logger.Logger.WithField("key", userConfig.Key).WithError(err).Error("Failed to decrypt key")
 364  				return err
 365  			}
 366  			clauses := clause.OnConflict{
 367  				Columns:   []clause.Column{{Name: "key"}},
 368  				DoUpdates: clause.AssignmentColumns([]string{"value"}),
 369  			}
 370  			err = cfg.set(userConfig.Key, decryptedValue, clauses, newUnlockPassword, tx)
 371  			if err != nil {
 372  				logger.Logger.WithField("key", userConfig.Key).WithError(err).Error("Failed to encrypt key")
 373  				return err
 374  			}
 375  			logger.Logger.WithField("key", userConfig.Key).Info("re-encrypted key")
 376  		}
 377  
 378  		// delete the JWT secret so it will be re-generated on next unlock (to log all sessions out on password change)
 379  		err = tx.Where(&db.UserConfig{Key: "JWTSecret"}).Delete(&db.UserConfig{}).Error
 380  		if err != nil {
 381  			logger.Logger.WithError(err).Error("failed to remove JWT secret during password change transaction")
 382  			return fmt.Errorf("failed to delete new JWT secret: %w", err)
 383  		}
 384  
 385  		logger.Logger.Info("Successfully removed JWT secret as part of password change transaction")
 386  		return nil
 387  	})
 388  
 389  	if err != nil {
 390  		logger.Logger.WithError(err).Error("failed to execute password change transaction")
 391  		return err
 392  	}
 393  
 394  	// JWT secret will be set on config unlock (required after password change)
 395  	cfg.jwtSecretMutex.Lock()
 396  	cfg.jwtSecret = ""
 397  	cfg.jwtSecretMutex.Unlock()
 398  	return nil
 399  }
 400  
 401  func (cfg *config) SetAutoUnlockPassword(unlockPassword string) error {
 402  	if unlockPassword != "" && !cfg.CheckUnlockPassword(unlockPassword) {
 403  		return errors.New("incorrect password")
 404  	}
 405  
 406  	err := cfg.SetUpdate("AutoUnlockPassword", unlockPassword, "")
 407  	if err != nil {
 408  		logger.Logger.WithError(err).Error("failed to update auto unlock password")
 409  		return err
 410  	}
 411  
 412  	return nil
 413  }
 414  
 415  // GetSessionSecret returns a persistent random secret used to sign NIP-07 UI
 416  // session tokens. It is stored unencrypted so it is available without the
 417  // unlock password, unlike the JWT secret used for password-based sessions.
 418  func (cfg *config) GetSessionSecret() (string, error) {
 419  	cfg.sessionSecretMutex.Lock()
 420  	defer cfg.sessionSecretMutex.Unlock()
 421  
 422  	if cfg.sessionSecret != "" {
 423  		return cfg.sessionSecret, nil
 424  	}
 425  
 426  	secret, err := cfg.Get("SessionSecret", "")
 427  	if err != nil {
 428  		return "", err
 429  	}
 430  	if secret == "" {
 431  		secret, err = randomHex(32)
 432  		if err != nil {
 433  			logger.Logger.WithError(err).Error("failed to generate session secret")
 434  			return "", err
 435  		}
 436  		err = cfg.SetUpdate("SessionSecret", secret, "")
 437  		if err != nil {
 438  			logger.Logger.WithError(err).Error("failed to save session secret")
 439  			return "", err
 440  		}
 441  	}
 442  
 443  	cfg.sessionSecret = secret
 444  	return secret, nil
 445  }
 446  
 447  // GetNip07OwnerPubkey returns the hex public key authorized to unlock the UI
 448  // via NIP-07. Empty when NIP07_PUBKEY is not configured.
 449  func (cfg *config) GetNip07OwnerPubkey() string {
 450  	configured := cfg.Env.Nip07Pubkey
 451  	if configured == "" {
 452  		return ""
 453  	}
 454  
 455  	if strings.HasPrefix(configured, "npub1") {
 456  		_, value, err := nip19.Decode(configured)
 457  		if err != nil {
 458  			logger.Logger.WithError(err).Error("failed to decode NIP07_PUBKEY")
 459  			return ""
 460  		}
 461  		hexPubkey, ok := value.(string)
 462  		if !ok {
 463  			logger.Logger.Error("NIP07_PUBKEY decoded to unexpected type")
 464  			return ""
 465  		}
 466  		return hexPubkey
 467  	}
 468  
 469  	return configured
 470  }
 471  
 472  func (cfg *config) CheckUnlockPassword(encryptionKey string) bool {
 473  	decryptedValue, err := cfg.Get("UnlockPasswordCheck", encryptionKey)
 474  
 475  	// require a non-empty match so an absent or empty canary always fails
 476  	return err == nil && decryptedValue != "" && decryptedValue == unlockPasswordCheck
 477  }
 478  
 479  func (cfg *config) IsUnlockPasswordCheckSet() (bool, error) {
 480  	// Read the raw value with an empty encryption key so we can detect the
 481  	// presence of the canary row without needing the (possibly wrong) password.
 482  	value, err := cfg.Get("UnlockPasswordCheck", "")
 483  	if err != nil {
 484  		return false, fmt.Errorf("read unlock password check: %w", err)
 485  	}
 486  	return value != "", nil
 487  }
 488  
 489  func (cfg *config) SaveUnlockPasswordCheck(encryptionKey string) error {
 490  	err := cfg.SetUpdate("UnlockPasswordCheck", unlockPasswordCheck, encryptionKey)
 491  	if err != nil {
 492  		logger.Logger.WithError(err).Error("Failed to save unlock password check to config")
 493  		return err
 494  	}
 495  	return nil
 496  }
 497  
 498  func (cfg *config) GetEnv() *AppConfig {
 499  	return cfg.Env
 500  }
 501  
 502  func randomHex(n int) (string, error) {
 503  	bytes := make([]byte, n)
 504  	if _, err := rand.Read(bytes); err != nil {
 505  		return "", err
 506  	}
 507  	return hex.EncodeToString(bytes), nil
 508  }
 509  
 510  const defaultCurrency = "USD"
 511  const defaultBitcoinDisplayFormat = constants.BITCOIN_DISPLAY_FORMAT_BIP177
 512  
 513  func (cfg *config) GetCurrency() string {
 514  	currency, err := cfg.Get("Currency", "")
 515  	if err != nil {
 516  		logger.Logger.WithError(err).Error("Failed to fetch currency")
 517  		return defaultCurrency
 518  	}
 519  	if currency == "" {
 520  		return defaultCurrency
 521  	}
 522  	return currency
 523  }
 524  
 525  func (cfg *config) SetCurrency(value string) error {
 526  	if value == "" {
 527  		return errors.New("currency value cannot be empty")
 528  	}
 529  	err := cfg.SetUpdate("Currency", value, "")
 530  	if err != nil {
 531  		logger.Logger.WithError(err).Error("Failed to update currency")
 532  		return err
 533  	}
 534  	return nil
 535  }
 536  
 537  func (cfg *config) GetBitcoinDisplayFormat() string {
 538  	format, err := cfg.Get("BitcoinDisplayFormat", "")
 539  	if err != nil {
 540  		logger.Logger.WithError(err).Error("Failed to fetch bitcoin display format")
 541  		return defaultBitcoinDisplayFormat
 542  	}
 543  	if format == "" {
 544  		return defaultBitcoinDisplayFormat
 545  	}
 546  	return format
 547  }
 548  
 549  func (cfg *config) SetBitcoinDisplayFormat(value string) error {
 550  	if value != constants.BITCOIN_DISPLAY_FORMAT_SATS && value != constants.BITCOIN_DISPLAY_FORMAT_BIP177 {
 551  		return fmt.Errorf("bitcoin display format must be '%s' or '%s'", constants.BITCOIN_DISPLAY_FORMAT_SATS, constants.BITCOIN_DISPLAY_FORMAT_BIP177)
 552  	}
 553  	err := cfg.SetUpdate("BitcoinDisplayFormat", value, "")
 554  	if err != nil {
 555  		logger.Logger.WithError(err).Error("Failed to update bitcoin display format")
 556  		return err
 557  	}
 558  	return nil
 559  }
 560