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