[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).
|
||||
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
|
||||
secret: unsecure_session_secret
|
||||
|
||||
|
|
|
@ -127,3 +127,8 @@ func (v *StructValidator) HasErrors() bool {
|
|||
func (v *StructValidator) Errors() []error {
|
||||
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
|
||||
}
|
||||
|
||||
if configuration.Secret == "" {
|
||||
if configuration.Redis != nil && configuration.Secret == "" {
|
||||
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)
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenPasswordNotSet(t *testing.T) {
|
||||
func TestShouldRaiseErrorWhenRedisIsUsedAndPasswordNotSet(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
config.Secret = ""
|
||||
|
||||
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.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 configuration.Redis != nil {
|
||||
providerName = "redis"
|
||||
serializer := NewEncryptingSerializer(configuration.Secret)
|
||||
providerConfig = &redis.Config{
|
||||
Host: configuration.Redis.Host,
|
||||
Port: configuration.Redis.Port,
|
||||
Password: configuration.Redis.Password,
|
||||
// DbNumber is the fasthttp/session property for the Redis DB Index
|
||||
DbNumber: configuration.Redis.DatabaseIndex,
|
||||
PoolSize: 8,
|
||||
IdleTimeout: 300,
|
||||
KeyPrefix: "authelia-session",
|
||||
DbNumber: configuration.Redis.DatabaseIndex,
|
||||
PoolSize: 8,
|
||||
IdleTimeout: 300,
|
||||
KeyPrefix: "authelia-session",
|
||||
SerializeFunc: serializer.Encode,
|
||||
UnSerializeFunc: serializer.Decode,
|
||||
}
|
||||
} else { // if no option is provided, use the memory provider.
|
||||
providerName = "memory"
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/redis"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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
|
||||
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