diff --git a/cipher/chacha20.go b/cipher/chacha20.go index ca576c356ae3..7f5c19b9bfaa 100644 --- a/cipher/chacha20.go +++ b/cipher/chacha20.go @@ -76,7 +76,7 @@ func (c *XChaCha20Poly1305) Decrypt(ctx context.Context, ciphertext string) ([]b return nil, errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReason("Unable to instantiate chacha20")) } - if len(ciphertext) < aead.NonceSize() { + if len(rawCiphertext) < aead.NonceSize() { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("cipher text too short")) } diff --git a/cipher/cipher_test.go b/cipher/cipher_test.go index a13ba60686f6..e64032b6fc65 100644 --- a/cipher/cipher_test.go +++ b/cipher/cipher_test.go @@ -73,6 +73,25 @@ func TestCipher(t *testing.T) { _, err = c.Decrypt(contextx.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}), "not-empty") require.Error(t, err) }) + + t.Run("case=short_ciphertext", func(t *testing.T) { + t.Parallel() + + // XChaCha20-Poly1305 has 24-byte nonce, hex encoded is 48 chars + // A valid ciphertext needs at least 24 bytes (nonce) + 16 bytes (tag) = 40 bytes minimum + // Hex encoded minimum is 80 chars + // This tests that we don't get panic on short ciphertext + + // 24 hex chars is only 12 bytes - less than nonce size (24 bytes) + shortCiphertext := "00112233445566778899aabbccddeeff" + _, err := c.Decrypt(ctx, shortCiphertext) + require.Error(t, err) + + // 64 hex chars is 32 bytes - still less than nonce(24)+tag(16)=40 + mediumCiphertext := "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff" + _, err = c.Decrypt(ctx, mediumCiphertext) + require.Error(t, err) + }) }) } diff --git a/driver/registry_default.go b/driver/registry_default.go index 3fceb0e63dd0..a9ef60358824 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -547,7 +547,13 @@ func (m *RegistryDefault) CookieManager(ctx context.Context) sessions.StoreExact func (m *RegistryDefault) ContinuityCookieManager(ctx context.Context) sessions.StoreExact { // To support hot reloading, this can not be instantiated only once. - cs := sessions.NewCookieStore(m.Config().SecretsSession(ctx)...) + var keys [][]byte + for _, k := range m.Config().SecretsSession(ctx) { + encrypt := sha256.Sum256(k) + keys = append(keys, k, encrypt[:]) + } + + cs := sessions.NewCookieStore(keys...) cs.Options.Secure = m.Config().CookieSecure(ctx) cs.Options.HttpOnly = true cs.Options.SameSite = http.SameSiteLaxMode diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index 4fc76e199067..2a4587cec9e4 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -6,6 +6,8 @@ package driver_test import ( "context" "fmt" + "net/http" + "net/http/httptest" "testing" "github.com/stretchr/testify/assert" @@ -969,3 +971,53 @@ func TestGetActiveVerificationStrategy(t *testing.T) { } }) } + +func TestContinuityCookieManager_Keys(t *testing.T) { + t.Parallel() + ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + + t.Run("supports key rotation", func(t *testing.T) { + t.Parallel() + + secret := "secret-key-32-bytes-long-xxxxx" + secret2 := "another-32-bytes-secret-keyyyy" + + // Step 1: Create registry with first secret and save a cookie + ctx1 := contextx.WithConfigValues(ctx, map[string]any{ + config.ViperKeySecretsCookie: []string{secret}, + }) + + store1 := reg.ContinuityCookieManager(ctx1) + require.NotNil(t, store1) + + // Save a session cookie + r1 := &http.Request{Header: http.Header{}} + w1 := httptest.NewRecorder() + session1, err := store1.New(r1, "test_session") + require.NoError(t, err) + session1.Values["test_key"] = "test_value" + err = session1.Save(r1, w1) + require.NoError(t, err) + + cookies := w1.Result().Cookies() + require.Len(t, cookies, 1) + cookie := cookies[0] + + // Step 2: Create registry with two secrets (new + old for rotation) + ctx2 := contextx.WithConfigValues(context.Background(), map[string]any{ + config.ViperKeySecretsCookie: []string{secret2, secret}, + }) + store2 := reg.ContinuityCookieManager(ctx2) + require.NotNil(t, store2) + + // Step 3: Read the original cookie with the new registry + // This should work because the second secret matches the original + r2 := &http.Request{Header: http.Header{}} + r2.AddCookie(cookie) + + session2, err := store2.Get(r2, "test_session") + require.NoError(t, err, "should be able to read cookie with rotated keys") + assert.Equal(t, "test_value", session2.Values["test_key"]) + }) +}