fix(api): use a 256-bit API v1 signing key

The HS256 key was 22 base62 characters, about 128 bits, below the 256 bits
RFC 7518 section 3.2 asks for. New keys are 32 bytes from crypto/rand,
hex-encoded before being encrypted and stored. Keys already stored keep
working unchanged.
This commit is contained in:
Deluan 2026-09-26 02:21:16 -04:00
commit 3e3b73a9cc
2 changed files with 27 additions and 2 deletions

View file

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

View file

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