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
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[:]
}

View file

@ -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() {

View file

@ -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()

View file

@ -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)