diff --git a/core/auth/auth.go b/core/auth/auth.go index 7725de8d6..e1fef5fa2 100644 --- a/core/auth/auth.go +++ b/core/auth/auth.go @@ -1,7 +1,9 @@ package auth import ( + "cmp" "context" + "crypto/sha256" "sync" "time" @@ -13,24 +15,32 @@ import ( "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" + "github.com/navidrome/navidrome/utils" ) var ( once sync.Once - Secret []byte TokenAuth *jwtauth.JWTAuth ) +// Init creates a JWTAuth object from the secret stored in the DB. +// If the secret is not found, it will create a new one and store it in the DB. func Init(ds model.DataStore) { once.Do(func() { + ctx := context.TODO() log.Info("Setting Session Timeout", "value", conf.Server.SessionTimeout) - secret, err := ds.Property(context.TODO()).Get(consts.JWTSecretKey) + + secret, err := ds.Property(ctx).Get(consts.JWTSecretKey) if err != nil || secret == "" { - log.Error("No JWT secret found in DB. Setting a temp one, but please report this error", err) - secret = uuid.NewString() + secret = createNewSecret(ctx, ds) + } else { + if secret, err = utils.Decrypt(ctx, getEncKey(), secret); err != nil { + log.Error(ctx, "Could not decrypt JWT secret, creating a new one", err) + secret = createNewSecret(ctx, ds) + } } - Secret = []byte(secret) - TokenAuth = jwtauth.New("HS256", Secret, nil) + + TokenAuth = jwtauth.New("HS256", []byte(secret), nil) }) } @@ -112,3 +122,27 @@ func WithAdminUser(ctx context.Context, ds model.DataStore) context.Context { ctx = request.WithUsername(ctx, u.UserName) return request.WithUser(ctx, *u) } + +func createNewSecret(ctx context.Context, ds model.DataStore) string { + log.Info(ctx, "Creating new JWT secret, used for encrypting UI sessions") + secret := uuid.NewString() + encSecret, err := utils.Encrypt(ctx, getEncKey(), secret) + if err != nil { + log.Error(ctx, "Could not encrypt JWT secret", err) + } + + if err := ds.Property(ctx).Put(consts.JWTSecretKey, encSecret); err != nil { + log.Error(ctx, "Could not save JWT secret in DB", err) + } + + return secret +} + +func getEncKey() []byte { + key := cmp.Or( + conf.Server.PasswordEncryptionKey, + consts.DefaultEncryptionKey, + ) + sum := sha256.Sum256([]byte(key)) + return sum[:] +} diff --git a/core/auth/auth_test.go b/core/auth/auth_test.go index a1908e128..504e56a52 100644 --- a/core/auth/auth_test.go +++ b/core/auth/auth_test.go @@ -4,12 +4,12 @@ import ( "testing" "time" - "github.com/go-chi/jwtauth/v5" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/core/auth" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/tests" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -32,8 +32,10 @@ var _ = BeforeSuite(func() { var _ = Describe("Auth", func() { BeforeEach(func() { - auth.Secret = []byte(testJWTSecret) - auth.TokenAuth = jwtauth.New("HS256", auth.Secret, nil) + ds := &tests.MockDataStore{ + MockedProperty: &tests.MockedPropertyRepo{}, + } + auth.Init(ds) }) Describe("Validate", func() { diff --git a/server/initial_setup.go b/server/initial_setup.go index a61e1a591..d0d21ec1d 100644 --- a/server/initial_setup.go +++ b/server/initial_setup.go @@ -27,10 +27,6 @@ func initialSetup(ds model.DataStore) { return nil } log.Info("Running initial setup") - if err = createJWTSecret(tx); err != nil { - return err - } - if conf.Server.DevAutoCreateAdminPassword != "" { if err = createInitialAdminUser(tx, conf.Server.DevAutoCreateAdminPassword); err != nil { return err @@ -69,20 +65,6 @@ func createInitialAdminUser(ds model.DataStore, initialPassword string) error { return err } -func createJWTSecret(ds model.DataStore) error { - properties := ds.Property(context.TODO()) - _, err := properties.Get(consts.JWTSecretKey) - if err == nil { - return nil - } - log.Info("Creating new JWT secret, used for encrypting UI sessions") - err = properties.Put(consts.JWTSecretKey, uuid.NewString()) - if err != nil { - log.Error("Could not save JWT secret in DB", err) - } - return err -} - func checkFFmpegInstallation() { f := ffmpeg.New() _, err := f.CmdPath() diff --git a/utils/encrypt.go b/utils/encrypt.go index e3185047e..98081baca 100644 --- a/utils/encrypt.go +++ b/utils/encrypt.go @@ -6,6 +6,7 @@ import ( "crypto/cipher" "crypto/rand" "encoding/base64" + "errors" "io" "github.com/navidrome/navidrome/log" @@ -36,7 +37,15 @@ func Encrypt(ctx context.Context, encKey []byte, data string) (string, error) { return base64.StdEncoding.EncodeToString(ciphertext), nil } -func Decrypt(ctx context.Context, encKey []byte, encData string) (string, error) { +func Decrypt(ctx context.Context, encKey []byte, encData string) (value string, err error) { + // Recover from any panics + defer func() { + if r := recover(); r != nil { + log.Error(ctx, "Panic during decryption", r) + err = errors.New("decryption panicked") + } + }() + enc, _ := base64.StdEncoding.DecodeString(encData) block, err := aes.NewCipher(encKey)