[MISC] Encrypt session data in redis store. (#789)
This is a regression from v3. With this change session data is encrypted with AES-GCM using a 256-bit key derived from the provided secret. Fixes #652.pull/790/head
parent
537378becc
commit
4264e64f9b
|
@ -245,7 +245,7 @@ session:
|
||||||
# The name of the session cookie. (default: authelia_session).
|
# The name of the session cookie. (default: authelia_session).
|
||||||
name: authelia_session
|
name: authelia_session
|
||||||
|
|
||||||
# The secret to encrypt the session cookie.
|
# The secret to encrypt the session data. This is only used with Redis.
|
||||||
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET
|
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET
|
||||||
secret: unsecure_session_secret
|
secret: unsecure_session_secret
|
||||||
|
|
||||||
|
|
|
@ -127,3 +127,8 @@ func (v *StructValidator) HasErrors() bool {
|
||||||
func (v *StructValidator) Errors() []error {
|
func (v *StructValidator) Errors() []error {
|
||||||
return v.errors
|
return v.errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear errors
|
||||||
|
func (v *StructValidator) Clear() {
|
||||||
|
v.errors = []error{}
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ func ValidateSession(configuration *schema.SessionConfiguration, validator *sche
|
||||||
configuration.Name = schema.DefaultSessionConfiguration.Name
|
configuration.Name = schema.DefaultSessionConfiguration.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
if configuration.Secret == "" {
|
if configuration.Redis != nil && configuration.Secret == "" {
|
||||||
validator.Push(errors.New("Set secret of the session object"))
|
validator.Push(errors.New("Set secret of the session object"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,13 +24,21 @@ func TestShouldSetDefaultSessionName(t *testing.T) {
|
||||||
assert.Equal(t, "authelia_session", config.Name)
|
assert.Equal(t, "authelia_session", config.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldRaiseErrorWhenPasswordNotSet(t *testing.T) {
|
func TestShouldRaiseErrorWhenRedisIsUsedAndPasswordNotSet(t *testing.T) {
|
||||||
validator := schema.NewStructValidator()
|
validator := schema.NewStructValidator()
|
||||||
config := newDefaultSessionConfig()
|
config := newDefaultSessionConfig()
|
||||||
config.Secret = ""
|
config.Secret = ""
|
||||||
|
|
||||||
ValidateSession(&config, validator)
|
ValidateSession(&config, validator)
|
||||||
|
|
||||||
|
assert.Len(t, validator.Errors(), 0)
|
||||||
|
validator.Clear()
|
||||||
|
|
||||||
|
// Set redis config because password must be set only when redis is used.
|
||||||
|
config.Redis = &schema.RedisSessionConfiguration{}
|
||||||
|
|
||||||
|
ValidateSession(&config, validator)
|
||||||
|
|
||||||
assert.Len(t, validator.Errors(), 1)
|
assert.Len(t, validator.Errors(), 1)
|
||||||
assert.EqualError(t, validator.Errors()[0], "Set secret of the session object")
|
assert.EqualError(t, validator.Errors()[0], "Set secret of the session object")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
|
"github.com/fasthttp/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The implementation of Encode and Decode method comes from
|
||||||
|
// https://github.com/gtank/cryptopasta/blob/master/hash.go
|
||||||
|
|
||||||
|
// EncryptingSerializer a serializer encrypting the data with AES-GCM with 256-bit keys.
|
||||||
|
type EncryptingSerializer struct {
|
||||||
|
key [32]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEncryptingSerializer return new encrypt instance
|
||||||
|
func NewEncryptingSerializer(secret string) *EncryptingSerializer {
|
||||||
|
key := sha256.Sum256([]byte(secret))
|
||||||
|
return &EncryptingSerializer{key}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode encode and encrypt session
|
||||||
|
func (e *EncryptingSerializer) Encode(src session.Dict) ([]byte, error) {
|
||||||
|
if len(src.D) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dst, err := src.MarshalMsg(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to marshal session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedDst, err := utils.Encrypt(dst, &e.key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to encrypt session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return encryptedDst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decrypt and decode session
|
||||||
|
func (e *EncryptingSerializer) Decode(dst *session.Dict, src []byte) error {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dst.Reset()
|
||||||
|
decryptedSrc, err := utils.Decrypt(src, &e.key)
|
||||||
|
if err != nil {
|
||||||
|
// If an error is thrown while decrypting, it's probably an old unencrypted session
|
||||||
|
// so we just unmarshall it without decrypting. It's a way to avoid a breaking change
|
||||||
|
// requiring to flush redis.
|
||||||
|
// TODO(clems4ever): remove in few months
|
||||||
|
_, uerr := dst.UnmarshalMsg(src)
|
||||||
|
if uerr != nil {
|
||||||
|
return fmt.Errorf("Unable to decrypt session: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dst.UnmarshalMsg(decryptedSrc)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/fasthttp/session"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShouldEncryptAndDecrypt(t *testing.T) {
|
||||||
|
payload := session.Dict{}
|
||||||
|
payload.Set("key", "value")
|
||||||
|
|
||||||
|
dst, err := payload.MarshalMsg(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
serializer := NewEncryptingSerializer("asecret")
|
||||||
|
encryptedDst, err := serializer.Encode(payload)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotEqual(t, dst, encryptedDst)
|
||||||
|
|
||||||
|
decodedPayload := session.Dict{}
|
||||||
|
err = serializer.Decode(&decodedPayload, encryptedDst)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "value", decodedPayload.Get("key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldSupportUnencryptedSessionForBackwardCompatibility(t *testing.T) {
|
||||||
|
payload := session.Dict{}
|
||||||
|
payload.Set("key", "value")
|
||||||
|
|
||||||
|
dst, err := payload.MarshalMsg(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
serializer := NewEncryptingSerializer("asecret")
|
||||||
|
|
||||||
|
decodedPayload := session.Dict{}
|
||||||
|
err = serializer.Decode(&decodedPayload, dst)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "value", decodedPayload.Get("key"))
|
||||||
|
}
|
|
@ -42,15 +42,18 @@ func NewProviderConfig(configuration schema.SessionConfiguration) ProviderConfig
|
||||||
// If redis configuration is provided, then use the redis provider.
|
// If redis configuration is provided, then use the redis provider.
|
||||||
if configuration.Redis != nil {
|
if configuration.Redis != nil {
|
||||||
providerName = "redis"
|
providerName = "redis"
|
||||||
|
serializer := NewEncryptingSerializer(configuration.Secret)
|
||||||
providerConfig = &redis.Config{
|
providerConfig = &redis.Config{
|
||||||
Host: configuration.Redis.Host,
|
Host: configuration.Redis.Host,
|
||||||
Port: configuration.Redis.Port,
|
Port: configuration.Redis.Port,
|
||||||
Password: configuration.Redis.Password,
|
Password: configuration.Redis.Password,
|
||||||
// DbNumber is the fasthttp/session property for the Redis DB Index
|
// DbNumber is the fasthttp/session property for the Redis DB Index
|
||||||
DbNumber: configuration.Redis.DatabaseIndex,
|
DbNumber: configuration.Redis.DatabaseIndex,
|
||||||
PoolSize: 8,
|
PoolSize: 8,
|
||||||
IdleTimeout: 300,
|
IdleTimeout: 300,
|
||||||
KeyPrefix: "authelia-session",
|
KeyPrefix: "authelia-session",
|
||||||
|
SerializeFunc: serializer.Encode,
|
||||||
|
UnSerializeFunc: serializer.Decode,
|
||||||
}
|
}
|
||||||
} else { // if no option is provided, use the memory provider.
|
} else { // if no option is provided, use the memory provider.
|
||||||
providerName = "memory"
|
providerName = "memory"
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
package session
|
package session
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/configuration/schema"
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
|
"github.com/fasthttp/session"
|
||||||
"github.com/fasthttp/session/memory"
|
"github.com/fasthttp/session/memory"
|
||||||
"github.com/fasthttp/session/redis"
|
"github.com/fasthttp/session/redis"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestShouldCreateInMemorySessionProvider(t *testing.T) {
|
func TestShouldCreateInMemorySessionProvider(t *testing.T) {
|
||||||
|
@ -76,3 +80,31 @@ func TestShouldSetDbNumber(t *testing.T) {
|
||||||
// DbNumber is the fasthttp/session property for the Redis DB Index
|
// DbNumber is the fasthttp/session property for the Redis DB Index
|
||||||
assert.Equal(t, 5, pConfig.DbNumber)
|
assert.Equal(t, 5, pConfig.DbNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShouldUseEncryptingSerializerWithRedis(t *testing.T) {
|
||||||
|
configuration := schema.SessionConfiguration{}
|
||||||
|
configuration.Secret = "abc"
|
||||||
|
configuration.Redis = &schema.RedisSessionConfiguration{
|
||||||
|
Host: "redis.example.com",
|
||||||
|
Port: 6379,
|
||||||
|
Password: "pass",
|
||||||
|
DatabaseIndex: 5,
|
||||||
|
}
|
||||||
|
providerConfig := NewProviderConfig(configuration)
|
||||||
|
pConfig := providerConfig.providerConfig.(*redis.Config)
|
||||||
|
|
||||||
|
payload := session.Dict{}
|
||||||
|
payload.Set("key", "value")
|
||||||
|
|
||||||
|
encoded, err := pConfig.SerializeFunc(payload)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Now we try to decrypt what has been serialized
|
||||||
|
key := sha256.Sum256([]byte("abc"))
|
||||||
|
decrypted, err := utils.Decrypt(encoded, &key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decoded := session.Dict{}
|
||||||
|
_, err = decoded.UnmarshalMsg(decrypted)
|
||||||
|
assert.Equal(t, "value", decoded.Get("key"))
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encrypt encrypts data using 256-bit AES-GCM. This both hides the content of
|
||||||
|
// the data and provides a check that it hasn't been altered. Output takes the
|
||||||
|
// form nonce|ciphertext|tag where '|' indicates concatenation.
|
||||||
|
func Encrypt(plaintext []byte, key *[32]byte) (ciphertext []byte, err error) {
|
||||||
|
block, err := aes.NewCipher(key[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
_, err = io.ReadFull(rand.Reader, nonce)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gcm.Seal(nonce, nonce, plaintext, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts data using 256-bit AES-GCM. This both hides the content of
|
||||||
|
// the data and provides a check that it hasn't been altered. Expects input
|
||||||
|
// form nonce|ciphertext|tag where '|' indicates concatenation.
|
||||||
|
func Decrypt(ciphertext []byte, key *[32]byte) (plaintext []byte, err error) {
|
||||||
|
block, err := aes.NewCipher(key[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ciphertext) < gcm.NonceSize() {
|
||||||
|
return nil, errors.New("malformed ciphertext")
|
||||||
|
}
|
||||||
|
|
||||||
|
return gcm.Open(nil,
|
||||||
|
ciphertext[:gcm.NonceSize()],
|
||||||
|
ciphertext[gcm.NonceSize():],
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue