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