diff --git a/core/apiauth/signer.go b/core/apiauth/signer.go index a2c21f39f..e4814d3c8 100644 --- a/core/apiauth/signer.go +++ b/core/apiauth/signer.go @@ -3,7 +3,9 @@ package apiauth import ( "cmp" "context" + "crypto/rand" "crypto/sha256" + "encoding/hex" "errors" "fmt" "strings" @@ -15,7 +17,6 @@ import ( "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" - "github.com/navidrome/navidrome/model/id" "github.com/navidrome/navidrome/utils" ) @@ -59,7 +60,7 @@ func loadSigner(ctx context.Context, ds model.DataStore, now func() time.Time) ( } func loadKey(ctx context.Context, ds model.DataStore) (string, error) { - enc, err := utils.Encrypt(ctx, encryptionKey(), id.NewRandom()) + enc, err := utils.Encrypt(ctx, encryptionKey(), newKey()) if err != nil { return "", fmt.Errorf("encrypting API v1 key: %w", err) } @@ -98,6 +99,13 @@ func loadKey(ctx context.Context, ds model.DataStore) (string, error) { return key, nil } +// newKey returns 256 random bits, the minimum RFC 7518 asks of an HS256 key. +func newKey() string { + b := make([]byte, 32) + _, _ = rand.Read(b) // never fails since Go 1.24 + return hex.EncodeToString(b) +} + func encryptionKey() []byte { sum := sha256.Sum256([]byte(cmp.Or(conf.Server.PasswordEncryptionKey, consts.DefaultEncryptionKey))) return sum[:] diff --git a/core/apiauth/signer_test.go b/core/apiauth/signer_test.go index acb197dde..b749d6193 100644 --- a/core/apiauth/signer_test.go +++ b/core/apiauth/signer_test.go @@ -2,12 +2,14 @@ package apiauth import ( "context" + "encoding/hex" "sync" "time" "github.com/lestrrat-go/jwx/v3/jwt" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/utils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -23,6 +25,21 @@ var _ = Describe("signer", func() { Expect(realDS.Property().Delete(ctx, consts.JWTAPIv1SecretKey)).To(Or(Succeed(), MatchError(model.ErrNotFound))) }) + It("creates a 256-bit key", func() { + key, err := loadKey(ctx, realDS) + Expect(err).ToNot(HaveOccurred()) + raw, err := hex.DecodeString(key) + Expect(err).ToNot(HaveOccurred()) + Expect(raw).To(HaveLen(32)) + }) + + It("keeps using a stored key created in the older format", func() { + enc, err := utils.Encrypt(ctx, encryptionKey(), "legacy22charskeyABCDEF") + Expect(err).ToNot(HaveOccurred()) + Expect(realDS.Property().Put(ctx, consts.JWTAPIv1SecretKey, enc)).To(Succeed()) + Expect(loadKey(ctx, realDS)).To(Equal("legacy22charskeyABCDEF")) + }) + It("round-trips claims", func() { s, err := loadSigner(ctx, realDS, clock) Expect(err).ToNot(HaveOccurred())