Derive every key from the signing key in one place (closes #315)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
seal.DeriveKeys derives the session hash and block keys, the encrypted URL key and the CSRF key from the signing key, with their salts declared beside it, and returns them as one seal.Keys. internal/providers calls it once at startup and provides the value to the fx graph. The session manager, the encrypted URL generator and the CSRF protection each take their own key. session.NewManager, encurl.NewGenerator, newCSRFProtect and handlers.New return no error, as nothing in them can fail now. Each key is derived as before, with seal.DeriveKey and the same salt. The signing key itself stays the HMAC key of signed URLs and the login password. README.md says under signing_key what it protects. Model: opus-5-5
This commit was merged in pull request #332.
This commit is contained in:
@@ -22,10 +22,6 @@ import (
|
||||
const (
|
||||
CookieName = "pixa_session"
|
||||
SessionTTL = 30 * 24 * time.Hour // 30 days
|
||||
|
||||
// HKDF salts for key derivation
|
||||
hashKeySalt = "pixa-session-hash-v1"
|
||||
blockKeySalt = "pixa-session-block-v1"
|
||||
)
|
||||
|
||||
// Errors returned by session operations.
|
||||
@@ -66,35 +62,23 @@ type Manager struct {
|
||||
logoutValue string
|
||||
}
|
||||
|
||||
// NewManager creates a session manager with keys derived from the signing key.
|
||||
// It makes a new random value and keeps it in memory only, so a restart ends
|
||||
// every session; NewManagerWithDatabase keeps the value in the database.
|
||||
// NewManager creates a session manager whose session cookies hashKey signs
|
||||
// and blockKey encrypts. It makes a new random value and keeps it in memory
|
||||
// only, so a restart ends every session; NewManagerWithDatabase keeps the
|
||||
// value in the database.
|
||||
//
|
||||
// Session cookies always carry the Secure, HttpOnly, and SameSite=Strict
|
||||
// attributes; this cannot be configured. Browsers treat http://localhost as a
|
||||
// trustworthy origin and accept Secure cookies there, so local development
|
||||
// keeps working.
|
||||
func NewManager(signingKey string) (*Manager, error) {
|
||||
masterKey := []byte(signingKey)
|
||||
|
||||
// Derive separate keys for HMAC (hash) and encryption (block)
|
||||
hashKey, err := seal.DeriveKey(masterKey, hashKeySalt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockKey, err := seal.DeriveKey(masterKey, blockKeySalt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func NewManager(hashKey, blockKey [seal.KeySize]byte) *Manager {
|
||||
sc := securecookie.New(hashKey[:], blockKey[:])
|
||||
sc.MaxAge(int(SessionTTL.Seconds()))
|
||||
|
||||
return &Manager{
|
||||
sc: sc,
|
||||
logoutValue: rand.Text(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewManagerWithDatabase creates a session manager like NewManager that keeps
|
||||
@@ -102,14 +86,11 @@ func NewManager(signingKey string) (*Manager, error) {
|
||||
// NewManager made when db has none, and otherwise starts from the one saved
|
||||
// there.
|
||||
func NewManagerWithDatabase(
|
||||
ctx context.Context, signingKey string, db *sql.DB,
|
||||
ctx context.Context, hashKey, blockKey [seal.KeySize]byte, db *sql.DB,
|
||||
) (*Manager, error) {
|
||||
m, err := NewManager(signingKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := NewManager(hashKey, blockKey)
|
||||
|
||||
_, err = db.ExecContext(ctx,
|
||||
_, err := db.ExecContext(ctx,
|
||||
"INSERT OR IGNORE INTO logout (id, logout_value) VALUES (1, ?)",
|
||||
m.logoutValue)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/seal"
|
||||
"sneak.berlin/go/pixa/internal/session"
|
||||
)
|
||||
|
||||
@@ -20,11 +21,13 @@ import (
|
||||
func TestSessionCookieAttributesAlwaysSecure(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr, err := session.NewManager("test-signing-key-12345")
|
||||
keys, err := seal.DeriveKeys("test-signing-key-12345")
|
||||
if err != nil {
|
||||
t.Fatalf("NewManager() error = %v", err)
|
||||
t.Fatalf("seal.DeriveKeys() error = %v", err)
|
||||
}
|
||||
|
||||
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
||||
|
||||
writePaths := []struct {
|
||||
name string
|
||||
setCookie func(t *testing.T, w http.ResponseWriter)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/database"
|
||||
"sneak.berlin/go/pixa/internal/seal"
|
||||
"sneak.berlin/go/pixa/internal/session"
|
||||
)
|
||||
|
||||
@@ -80,8 +81,13 @@ func TestLogout_EndsEverySessionMadeBeforeIt(t *testing.T) {
|
||||
|
||||
db := newTestDatabase(t)
|
||||
|
||||
keys, err := seal.DeriveKeys(logoutTestSigningKey)
|
||||
if err != nil {
|
||||
t.Fatalf("seal.DeriveKeys() error = %v", err)
|
||||
}
|
||||
|
||||
mgr, err := session.NewManagerWithDatabase(
|
||||
t.Context(), logoutTestSigningKey, db)
|
||||
t.Context(), keys.SessionHashKey, keys.SessionBlockKey, db)
|
||||
if err != nil {
|
||||
t.Fatalf("NewManagerWithDatabase() error = %v", err)
|
||||
}
|
||||
@@ -109,7 +115,7 @@ func TestLogout_EndsEverySessionMadeBeforeIt(t *testing.T) {
|
||||
}
|
||||
|
||||
restarted, err := session.NewManagerWithDatabase(
|
||||
t.Context(), logoutTestSigningKey, db)
|
||||
t.Context(), keys.SessionHashKey, keys.SessionBlockKey, db)
|
||||
if err != nil {
|
||||
t.Fatalf("NewManagerWithDatabase() after restart error = %v", err)
|
||||
}
|
||||
@@ -129,8 +135,13 @@ func TestLogout_EndsEverySessionMadeBeforeIt(t *testing.T) {
|
||||
func TestLogout_WithoutSession_EndsNoSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr, err := session.NewManagerWithDatabase(
|
||||
t.Context(), logoutTestSigningKey, newTestDatabase(t))
|
||||
keys, err := seal.DeriveKeys(logoutTestSigningKey)
|
||||
if err != nil {
|
||||
t.Fatalf("seal.DeriveKeys() error = %v", err)
|
||||
}
|
||||
|
||||
mgr, err := session.NewManagerWithDatabase(t.Context(),
|
||||
keys.SessionHashKey, keys.SessionBlockKey, newTestDatabase(t))
|
||||
if err != nil {
|
||||
t.Fatalf("NewManagerWithDatabase() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -7,17 +7,20 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/seal"
|
||||
"sneak.berlin/go/pixa/internal/session"
|
||||
)
|
||||
|
||||
func TestManager_CreateAndValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr, err := session.NewManager("test-signing-key-12345")
|
||||
keys, err := seal.DeriveKeys("test-signing-key-12345")
|
||||
if err != nil {
|
||||
t.Fatalf("NewManager() error = %v", err)
|
||||
t.Fatalf("seal.DeriveKeys() error = %v", err)
|
||||
}
|
||||
|
||||
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
||||
|
||||
// Create a session
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@@ -69,7 +72,8 @@ func TestManager_CreateAndValidate(t *testing.T) {
|
||||
func TestManager_ValidateSession_NoCookie(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr, _ := session.NewManager("test-signing-key-12345")
|
||||
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
||||
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
||||
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
||||
|
||||
@@ -86,7 +90,8 @@ func TestManager_ValidateSession_NoCookie(t *testing.T) {
|
||||
func TestManager_ValidateSession_TamperedCookie(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr, _ := session.NewManager("test-signing-key-12345")
|
||||
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
||||
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
||||
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
||||
req.AddCookie(&http.Cookie{
|
||||
@@ -110,8 +115,10 @@ func TestManager_ValidateSession_TamperedCookie(t *testing.T) {
|
||||
func TestManager_ValidateSession_WrongKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr1, _ := session.NewManager("signing-key-1")
|
||||
mgr2, _ := session.NewManager("signing-key-2")
|
||||
keys1, _ := seal.DeriveKeys("signing-key-1")
|
||||
keys2, _ := seal.DeriveKeys("signing-key-2")
|
||||
mgr1 := session.NewManager(keys1.SessionHashKey, keys1.SessionBlockKey)
|
||||
mgr2 := session.NewManager(keys2.SessionHashKey, keys2.SessionBlockKey)
|
||||
|
||||
// Create session with mgr1
|
||||
w := httptest.NewRecorder()
|
||||
@@ -142,7 +149,8 @@ func TestManager_ValidateSession_WrongKey(t *testing.T) {
|
||||
func TestManager_ClearSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr, _ := session.NewManager("test-signing-key-12345")
|
||||
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
||||
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
mgr.ClearSession(w)
|
||||
@@ -172,7 +180,8 @@ func TestManager_ClearSession(t *testing.T) {
|
||||
func TestManager_IsAuthenticated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr, _ := session.NewManager("test-signing-key-12345")
|
||||
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
||||
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
||||
|
||||
// No session - should return false
|
||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
||||
@@ -208,7 +217,8 @@ func TestManager_IsAuthenticated(t *testing.T) {
|
||||
func TestManager_CookieAttributes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr, _ := session.NewManager("test-key")
|
||||
keys, _ := seal.DeriveKeys("test-key")
|
||||
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
_ = mgr.CreateSession(w)
|
||||
|
||||
Reference in New Issue
Block a user