fix(server): encrypt jwt secret at rest

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2024-12-05 21:40:34 -05:00
parent 177a1f853f
commit 7f030b0859
4 changed files with 55 additions and 28 deletions

View file

@ -1,7 +1,9 @@
package auth package auth
import ( import (
"cmp"
"context" "context"
"crypto/sha256"
"sync" "sync"
"time" "time"
@ -13,24 +15,32 @@ import (
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils"
) )
var ( var (
once sync.Once once sync.Once
Secret []byte
TokenAuth *jwtauth.JWTAuth 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) { func Init(ds model.DataStore) {
once.Do(func() { once.Do(func() {
ctx := context.TODO()
log.Info("Setting Session Timeout", "value", conf.Server.SessionTimeout) 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 == "" { if err != nil || secret == "" {
log.Error("No JWT secret found in DB. Setting a temp one, but please report this error", err) secret = createNewSecret(ctx, ds)
secret = uuid.NewString() } 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) ctx = request.WithUsername(ctx, u.UserName)
return request.WithUser(ctx, *u) 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[:]
}

View file

@ -4,12 +4,12 @@ import (
"testing" "testing"
"time" "time"
"github.com/go-chi/jwtauth/v5"
"github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/auth" "github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
@ -32,8 +32,10 @@ var _ = BeforeSuite(func() {
var _ = Describe("Auth", func() { var _ = Describe("Auth", func() {
BeforeEach(func() { BeforeEach(func() {
auth.Secret = []byte(testJWTSecret) ds := &tests.MockDataStore{
auth.TokenAuth = jwtauth.New("HS256", auth.Secret, nil) MockedProperty: &tests.MockedPropertyRepo{},
}
auth.Init(ds)
}) })
Describe("Validate", func() { Describe("Validate", func() {

View file

@ -27,10 +27,6 @@ func initialSetup(ds model.DataStore) {
return nil return nil
} }
log.Info("Running initial setup") log.Info("Running initial setup")
if err = createJWTSecret(tx); err != nil {
return err
}
if conf.Server.DevAutoCreateAdminPassword != "" { if conf.Server.DevAutoCreateAdminPassword != "" {
if err = createInitialAdminUser(tx, conf.Server.DevAutoCreateAdminPassword); err != nil { if err = createInitialAdminUser(tx, conf.Server.DevAutoCreateAdminPassword); err != nil {
return err return err
@ -69,20 +65,6 @@ func createInitialAdminUser(ds model.DataStore, initialPassword string) error {
return err 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() { func checkFFmpegInstallation() {
f := ffmpeg.New() f := ffmpeg.New()
_, err := f.CmdPath() _, err := f.CmdPath()

View file

@ -6,6 +6,7 @@ import (
"crypto/cipher" "crypto/cipher"
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"errors"
"io" "io"
"github.com/navidrome/navidrome/log" "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 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) enc, _ := base64.StdEncoding.DecodeString(encData)
block, err := aes.NewCipher(encKey) block, err := aes.NewCipher(encKey)