type: string
diff --git a/docs/configuration/storage/postgres.md b/docs/configuration/storage/postgres.md
index 525aeeb91..788027cf5 100644
--- a/docs/configuration/storage/postgres.md
+++ b/docs/configuration/storage/postgres.md
@@ -14,6 +14,7 @@ The PostgreSQL storage provider.
```yaml
storage:
+ encryption_key: a_very_important_secret
postgres:
host: 127.0.0.1
port: 5432
@@ -25,6 +26,9 @@ storage:
## Options
+### encryption_key
+See the [encryption_key docs](./index.md#encryption_key).
+
### host
type: string
diff --git a/docs/configuration/storage/sqlite.md b/docs/configuration/storage/sqlite.md
index 7fa8c42b7..21bd34a89 100644
--- a/docs/configuration/storage/sqlite.md
+++ b/docs/configuration/storage/sqlite.md
@@ -20,12 +20,16 @@ requires you setup an external database.
```yaml
storage:
+ encryption_key: a_very_important_secret
local:
path: /config/db.sqlite3
```
## Options
+### encryption_key
+See the [encryption_key docs](./index.md#encryption_key).
+
### path
type: string
diff --git a/internal/commands/const.go b/internal/commands/const.go
index c8cd5d1b5..5a5317e69 100644
--- a/internal/commands/const.go
+++ b/internal/commands/const.go
@@ -1,5 +1,9 @@
package commands
+import (
+ "errors"
+)
+
const cmdAutheliaExample = `authelia --config /etc/authelia/config.yml --config /etc/authelia/access-control.yml
authelia --config /etc/authelia/config.yml,/etc/authelia/access-control.yml
authelia --config /etc/authelia/config/
@@ -80,3 +84,12 @@ const (
storageMigrateDirectionUp = "up"
storageMigrateDirectionDown = "down"
)
+
+const (
+ storageExportFormatCSV = "csv"
+ storageExportFormatURI = "uri"
+)
+
+var (
+ errNoStorageProvider = errors.New("no storage provider configured")
+)
diff --git a/internal/commands/helpers.go b/internal/commands/helpers.go
index e90c78464..70ed2058f 100644
--- a/internal/commands/helpers.go
+++ b/internal/commands/helpers.go
@@ -1,28 +1,84 @@
package commands
import (
- "errors"
-
+ "github.com/authelia/authelia/v4/internal/authentication"
+ "github.com/authelia/authelia/v4/internal/authorization"
+ "github.com/authelia/authelia/v4/internal/middlewares"
+ "github.com/authelia/authelia/v4/internal/notification"
+ "github.com/authelia/authelia/v4/internal/ntp"
+ "github.com/authelia/authelia/v4/internal/oidc"
+ "github.com/authelia/authelia/v4/internal/regulation"
+ "github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/storage"
+ "github.com/authelia/authelia/v4/internal/utils"
)
-func getStorageProvider() (provider storage.Provider, err error) {
+func getStorageProvider() (provider storage.Provider) {
switch {
case config.Storage.PostgreSQL != nil:
- provider = storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL)
+ return storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL, config.Storage.EncryptionKey)
case config.Storage.MySQL != nil:
- provider = storage.NewMySQLProvider(*config.Storage.MySQL)
+ return storage.NewMySQLProvider(*config.Storage.MySQL, config.Storage.EncryptionKey)
case config.Storage.Local != nil:
- provider = storage.NewSQLiteProvider(config.Storage.Local.Path)
+ return storage.NewSQLiteProvider(config.Storage.Local.Path, config.Storage.EncryptionKey)
default:
- return nil, errors.New("no storage provider configured")
+ return nil
}
-
- if (config.Storage.MySQL != nil && config.Storage.PostgreSQL != nil) ||
- (config.Storage.MySQL != nil && config.Storage.Local != nil) ||
- (config.Storage.PostgreSQL != nil && config.Storage.Local != nil) {
- return nil, errors.New("multiple storage providers are configured but should only configure one")
- }
-
- return provider, err
+}
+
+func getProviders() (providers middlewares.Providers, warnings []error, errors []error) {
+ // TODO: Adjust this so the CertPool can be used like a provider.
+ autheliaCertPool, warnings, errors := utils.NewX509CertPool(config.CertificatesDirectory)
+ if len(warnings) != 0 || len(errors) != 0 {
+ return providers, warnings, errors
+ }
+
+ storageProvider := getStorageProvider()
+
+ var (
+ userProvider authentication.UserProvider
+ err error
+ )
+
+ switch {
+ case config.AuthenticationBackend.File != nil:
+ userProvider = authentication.NewFileUserProvider(config.AuthenticationBackend.File)
+ case config.AuthenticationBackend.LDAP != nil:
+ userProvider = authentication.NewLDAPUserProvider(config.AuthenticationBackend, autheliaCertPool)
+ }
+
+ var notifier notification.Notifier
+
+ switch {
+ case config.Notifier.SMTP != nil:
+ notifier = notification.NewSMTPNotifier(config.Notifier.SMTP, autheliaCertPool)
+ case config.Notifier.FileSystem != nil:
+ notifier = notification.NewFileNotifier(*config.Notifier.FileSystem)
+ }
+
+ var ntpProvider *ntp.Provider
+ if config.NTP != nil {
+ ntpProvider = ntp.NewProvider(config.NTP)
+ }
+
+ clock := utils.RealClock{}
+ authorizer := authorization.NewAuthorizer(config)
+ sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
+ regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
+
+ oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
+ if err != nil {
+ errors = append(errors, err)
+ }
+
+ return middlewares.Providers{
+ Authorizer: authorizer,
+ UserProvider: userProvider,
+ Regulator: regulator,
+ OpenIDConnect: oidcProvider,
+ StorageProvider: storageProvider,
+ NTP: ntpProvider,
+ Notifier: notifier,
+ SessionProvider: sessionProvider,
+ }, warnings, errors
}
diff --git a/internal/commands/helpers_test.go b/internal/commands/helpers_test.go
new file mode 100644
index 000000000..797ecfd6c
--- /dev/null
+++ b/internal/commands/helpers_test.go
@@ -0,0 +1,15 @@
+package commands
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/authelia/authelia/v4/internal/configuration/schema"
+)
+
+func TestGetStorageProvider(t *testing.T) {
+ config = &schema.Configuration{}
+
+ assert.Nil(t, getStorageProvider())
+}
diff --git a/internal/commands/root.go b/internal/commands/root.go
index b3c96641a..a347428ad 100644
--- a/internal/commands/root.go
+++ b/internal/commands/root.go
@@ -8,19 +8,11 @@ import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
- "github.com/authelia/authelia/v4/internal/authentication"
- "github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/logging"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/models"
- "github.com/authelia/authelia/v4/internal/notification"
- "github.com/authelia/authelia/v4/internal/ntp"
- "github.com/authelia/authelia/v4/internal/oidc"
- "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/server"
- "github.com/authelia/authelia/v4/internal/session"
- "github.com/authelia/authelia/v4/internal/storage"
"github.com/authelia/authelia/v4/internal/utils"
)
@@ -67,7 +59,7 @@ func cmdRootRun(_ *cobra.Command, _ []string) {
logger.Fatalf("Cannot initialize logger: %v", err)
}
- providers, warnings, errors := getProviders(config)
+ providers, warnings, errors := getProviders()
if len(warnings) != 0 {
for _, err := range warnings {
logger.Warn(err)
@@ -87,72 +79,6 @@ func cmdRootRun(_ *cobra.Command, _ []string) {
server.Start(*config, providers)
}
-func getProviders(config *schema.Configuration) (providers middlewares.Providers, warnings []error, errors []error) {
- // TODO: Adjust this so the CertPool can be used like a provider.
- autheliaCertPool, warnings, errors := utils.NewX509CertPool(config.CertificatesDirectory)
- if len(warnings) != 0 || len(errors) != 0 {
- return providers, warnings, errors
- }
-
- var storageProvider storage.Provider
-
- switch {
- case config.Storage.PostgreSQL != nil:
- storageProvider = storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL)
- case config.Storage.MySQL != nil:
- storageProvider = storage.NewMySQLProvider(*config.Storage.MySQL)
- case config.Storage.Local != nil:
- storageProvider = storage.NewSQLiteProvider(config.Storage.Local.Path)
- }
-
- var (
- userProvider authentication.UserProvider
- err error
- )
-
- switch {
- case config.AuthenticationBackend.File != nil:
- userProvider = authentication.NewFileUserProvider(config.AuthenticationBackend.File)
- case config.AuthenticationBackend.LDAP != nil:
- userProvider = authentication.NewLDAPUserProvider(config.AuthenticationBackend, autheliaCertPool)
- }
-
- var notifier notification.Notifier
-
- switch {
- case config.Notifier.SMTP != nil:
- notifier = notification.NewSMTPNotifier(config.Notifier.SMTP, autheliaCertPool)
- case config.Notifier.FileSystem != nil:
- notifier = notification.NewFileNotifier(*config.Notifier.FileSystem)
- }
-
- var ntpProvider *ntp.Provider
- if config.NTP != nil {
- ntpProvider = ntp.NewProvider(config.NTP)
- }
-
- clock := utils.RealClock{}
- authorizer := authorization.NewAuthorizer(config)
- sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
- regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
-
- oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
- if err != nil {
- errors = append(errors, err)
- }
-
- return middlewares.Providers{
- Authorizer: authorizer,
- UserProvider: userProvider,
- Regulator: regulator,
- OpenIDConnect: oidcProvider,
- StorageProvider: storageProvider,
- NTP: ntpProvider,
- Notifier: notifier,
- SessionProvider: sessionProvider,
- }, warnings, errors
-}
-
func doStartupChecks(config *schema.Configuration, providers *middlewares.Providers) {
logger := logging.Logger()
diff --git a/internal/commands/storage.go b/internal/commands/storage.go
index f6b62e5a8..8d3cc2e13 100644
--- a/internal/commands/storage.go
+++ b/internal/commands/storage.go
@@ -15,6 +15,8 @@ func NewStorageCmd() (cmd *cobra.Command) {
cmd.PersistentFlags().StringSliceP("config", "c", []string{"config.yml"}, "configuration file to load for the storage migration")
+ cmd.PersistentFlags().String("encryption-key", "", "the storage encryption key to use")
+
cmd.PersistentFlags().String("sqlite.path", "", "the SQLite database path")
cmd.PersistentFlags().String("mysql.host", "", "the MySQL hostname")
@@ -32,11 +34,74 @@ func NewStorageCmd() (cmd *cobra.Command) {
cmd.AddCommand(
newStorageMigrateCmd(),
newStorageSchemaInfoCmd(),
+ newStorageEncryptionCmd(),
+ newStorageExportCmd(),
)
return cmd
}
+func newStorageEncryptionCmd() (cmd *cobra.Command) {
+ cmd = &cobra.Command{
+ Use: "encryption",
+ Short: "Manages encryption",
+ }
+
+ cmd.AddCommand(
+ newStorageEncryptionChangeKeyCmd(),
+ newStorageEncryptionCheckCmd(),
+ )
+
+ return cmd
+}
+
+func newStorageEncryptionCheckCmd() (cmd *cobra.Command) {
+ cmd = &cobra.Command{
+ Use: "check",
+ Short: "Checks the encryption key against the database data",
+ RunE: storageSchemaEncryptionCheckRunE,
+ }
+
+ cmd.Flags().Bool("verbose", false, "enables verbose checking of every row of encrypted data")
+
+ return cmd
+}
+
+func newStorageEncryptionChangeKeyCmd() (cmd *cobra.Command) {
+ cmd = &cobra.Command{
+ Use: "change-key",
+ Short: "Changes the encryption key",
+ RunE: storageSchemaEncryptionChangeKeyRunE,
+ }
+
+ cmd.Flags().String("new-encryption-key", "", "the new key to encrypt the data with")
+
+ return cmd
+}
+
+func newStorageExportCmd() (cmd *cobra.Command) {
+ cmd = &cobra.Command{
+ Use: "export",
+ Short: "Performs exports",
+ }
+
+ cmd.AddCommand(newStorageExportTOTPConfigurationsCmd())
+
+ return cmd
+}
+
+func newStorageExportTOTPConfigurationsCmd() (cmd *cobra.Command) {
+ cmd = &cobra.Command{
+ Use: "totp-configurations",
+ Short: "Performs exports of the totp configurations",
+ RunE: storageExportTOTPConfigurationsRunE,
+ }
+
+ cmd.Flags().String("format", storageExportFormatCSV, "changes the format of the export, options are csv and uri")
+
+ return cmd
+}
+
func newStorageSchemaInfoCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "schema-info",
diff --git a/internal/commands/storage_run.go b/internal/commands/storage_run.go
index 681683401..0af2e7906 100644
--- a/internal/commands/storage_run.go
+++ b/internal/commands/storage_run.go
@@ -12,6 +12,7 @@ import (
"github.com/authelia/authelia/v4/internal/configuration"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/configuration/validator"
+ "github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/storage"
)
@@ -38,6 +39,7 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
}
mapping := map[string]string{
+ "encryption-key": "storage.encryption_key",
"sqlite.path": "storage.local.path",
"mysql.host": "storage.mysql.host",
"mysql.port": "storage.mysql.port",
@@ -100,17 +102,179 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
return nil
}
+func storageSchemaEncryptionCheckRunE(cmd *cobra.Command, args []string) (err error) {
+ var (
+ provider storage.Provider
+ ctx = context.Background()
+ )
+
+ provider = getStorageProvider()
+ if provider == nil {
+ return errNoStorageProvider
+ }
+
+ defer func() {
+ _ = provider.Close()
+ }()
+
+ verbose, err := cmd.Flags().GetBool("verbose")
+ if err != nil {
+ return err
+ }
+
+ if err = provider.SchemaEncryptionCheckKey(ctx, verbose); err != nil {
+ switch {
+ case errors.Is(err, storage.ErrSchemaEncryptionVersionUnsupported):
+ fmt.Printf("Could not check encryption key for validity. The schema version doesn't support encryption.\n")
+ case errors.Is(err, storage.ErrSchemaEncryptionInvalidKey):
+ fmt.Printf("Encryption key validation: failed.\n\nError: %v.\n", err)
+ default:
+ fmt.Printf("Could not check encryption key for validity.\n\nError: %v.\n", err)
+ }
+ } else {
+ fmt.Println("Encryption key validation: success.")
+ }
+
+ return nil
+}
+
+func storageSchemaEncryptionChangeKeyRunE(cmd *cobra.Command, args []string) (err error) {
+ var (
+ provider storage.Provider
+ ctx = context.Background()
+ )
+
+ provider = getStorageProvider()
+ if provider == nil {
+ return errNoStorageProvider
+ }
+
+ defer func() {
+ _ = provider.Close()
+ }()
+
+ if err = checkStorageSchemaUpToDate(ctx, provider); err != nil {
+ return err
+ }
+
+ version, err := provider.SchemaVersion(ctx)
+ if err != nil {
+ return err
+ }
+
+ if version <= 0 {
+ return errors.New("schema version must be at least version 1 to change the encryption key")
+ }
+
+ key, err := cmd.Flags().GetString("new-encryption-key")
+ if err != nil {
+ return err
+ }
+
+ if key == "" {
+ return errors.New("you must set the --new-encryption-key flag")
+ }
+
+ if len(key) < 20 {
+ return errors.New("the encryption key must be at least 20 characters")
+ }
+
+ if err = provider.SchemaEncryptionChangeKey(ctx, key); err != nil {
+ return err
+ }
+
+ fmt.Println("Completed the encryption key change. Please adjust your configuration to use the new key.")
+
+ return nil
+}
+
+func storageExportTOTPConfigurationsRunE(cmd *cobra.Command, args []string) (err error) {
+ var (
+ provider storage.Provider
+ ctx = context.Background()
+ )
+
+ provider = getStorageProvider()
+ if provider == nil {
+ return errNoStorageProvider
+ }
+
+ defer func() {
+ _ = provider.Close()
+ }()
+
+ if err = checkStorageSchemaUpToDate(ctx, provider); err != nil {
+ return err
+ }
+
+ format, err := cmd.Flags().GetString("format")
+ if err != nil {
+ return err
+ }
+
+ switch format {
+ case storageExportFormatCSV, storageExportFormatURI:
+ break
+ default:
+ return errors.New("format must be csv or uri")
+ }
+
+ limit := 10
+
+ var configurations []models.TOTPConfiguration
+
+ for page := 0; true; page++ {
+ configurations, err = provider.LoadTOTPConfigurations(ctx, limit, page)
+ if err != nil {
+ return err
+ }
+
+ if page == 0 && format == storageExportFormatCSV {
+ fmt.Printf("issuer,username,algorithm,digits,period,secret\n")
+ }
+
+ for _, c := range configurations {
+ switch format {
+ case storageExportFormatCSV:
+ fmt.Printf("%s,%s,%s,%d,%d,%s\n", "Authelia", c.Username, c.Algorithm, c.Digits, c.Period, string(c.Secret))
+ case storageExportFormatURI:
+ fmt.Printf("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=%s&digits=%d&period=%d\n", "Authelia", c.Username, string(c.Secret), "Authelia", c.Algorithm, c.Digits, c.Period)
+ }
+ }
+
+ if len(configurations) < limit {
+ break
+ }
+ }
+
+ return nil
+}
+
func storageMigrateHistoryRunE(_ *cobra.Command, _ []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
)
- provider, err = getStorageProvider()
+ provider = getStorageProvider()
+ if provider == nil {
+ return errNoStorageProvider
+ }
+
+ defer func() {
+ _ = provider.Close()
+ }()
+
+ version, err := provider.SchemaVersion(ctx)
if err != nil {
return err
}
+ if version <= 0 {
+ fmt.Println("No migration history is available for schemas that not version 1 or above.")
+ return
+ }
+
migrations, err := provider.SchemaMigrationHistory(ctx)
if err != nil {
return err
@@ -138,11 +302,15 @@ func newStorageMigrateListRunE(up bool) func(cmd *cobra.Command, args []string)
directionStr string
)
- provider, err = getStorageProvider()
- if err != nil {
- return err
+ provider = getStorageProvider()
+ if provider == nil {
+ return errNoStorageProvider
}
+ defer func() {
+ _ = provider.Close()
+ }()
+
if up {
migrations, err = provider.SchemaMigrationsUp(ctx, 0)
directionStr = "Up"
@@ -151,13 +319,7 @@ func newStorageMigrateListRunE(up bool) func(cmd *cobra.Command, args []string)
directionStr = "Down"
}
- if err != nil {
- if err.Error() == "cannot migrate to the same version as prior" {
- fmt.Printf("No %s migrations found\n", directionStr)
-
- return nil
- }
-
+ if err != nil && !errors.Is(err, storage.ErrNoAvailableMigrations) && !errors.Is(err, storage.ErrMigrateCurrentVersionSameAsTarget) {
return err
}
@@ -182,11 +344,15 @@ func newStorageMigrationRunE(up bool) func(cmd *cobra.Command, args []string) (e
ctx = context.Background()
)
- provider, err = getStorageProvider()
- if err != nil {
- return err
+ provider = getStorageProvider()
+ if provider == nil {
+ return errNoStorageProvider
}
+ defer func() {
+ _ = provider.Close()
+ }()
+
target, err := cmd.Flags().GetInt("target")
if err != nil {
return err
@@ -253,11 +419,15 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
tablesStr string
)
- provider, err = getStorageProvider()
- if err != nil {
- return err
+ provider = getStorageProvider()
+ if provider == nil {
+ return errNoStorageProvider
}
+ defer func() {
+ _ = provider.Close()
+ }()
+
version, err := provider.SchemaVersion(ctx)
if err != nil && err.Error() != "unknown schema state" {
return err
@@ -285,7 +455,37 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
upgradeStr = "no"
}
- fmt.Printf("Schema Version: %s\nSchema Upgrade Available: %s\nSchema Tables: %s\n", storage.SchemaVersionToString(version), upgradeStr, tablesStr)
+ var encryption string
+
+ if err = provider.SchemaEncryptionCheckKey(ctx, false); err != nil {
+ if errors.Is(err, storage.ErrSchemaEncryptionVersionUnsupported) {
+ encryption = "unsupported (schema version)"
+ } else {
+ encryption = "invalid"
+ }
+ } else {
+ encryption = "valid"
+ }
+
+ fmt.Printf("Schema Version: %s\nSchema Upgrade Available: %s\nSchema Tables: %s\nSchema Encryption Key: %s\n", storage.SchemaVersionToString(version), upgradeStr, tablesStr, encryption)
+
+ return nil
+}
+
+func checkStorageSchemaUpToDate(ctx context.Context, provider storage.Provider) (err error) {
+ version, err := provider.SchemaVersion(ctx)
+ if err != nil {
+ return err
+ }
+
+ latest, err := provider.SchemaLatestVersion()
+ if err != nil {
+ return err
+ }
+
+ if version != latest {
+ return fmt.Errorf("schema is version %d which is outdated please migrate to version %d in order to use this command or use an older binary", version, latest)
+ }
return nil
}
diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml
index 2fa3825e9..578701a8a 100644
--- a/internal/configuration/config.template.yml
+++ b/internal/configuration/config.template.yml
@@ -507,6 +507,10 @@ regulation:
##
## The available providers are: `local`, `mysql`, `postgres`. You must use one and only one of these providers.
storage:
+ ## The encryption key that is used to encrypt sensitive information in the database. Must be a string with a minimum
+ ## length of 20. Please see the docs if you configure this with an undesirable key and need to change it.
+ # encryption_key: you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this
+
##
## Local (Storage Provider)
##
diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go
index d4fc155db..5d800e7ea 100644
--- a/internal/configuration/provider_test.go
+++ b/internal/configuration/provider_test.go
@@ -138,6 +138,7 @@ func TestShouldValidateAndRaiseErrorsOnNormalConfigurationAndSecret(t *testing.T
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "an env storage mysql password"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", "./test_resources/example_secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "an env authentication backend ldap password"))
+ assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_ENCRYPTION_KEY", "a_very_bad_encryption_key"))
val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
@@ -152,6 +153,7 @@ func TestShouldValidateAndRaiseErrorsOnNormalConfigurationAndSecret(t *testing.T
assert.Equal(t, "example_secret value", config.Session.Secret)
assert.Equal(t, "an env storage mysql password", config.Storage.MySQL.Password)
assert.Equal(t, "an env authentication backend ldap password", config.AuthenticationBackend.LDAP.Password)
+ assert.Equal(t, "a_very_bad_encryption_key", config.Storage.EncryptionKey)
}
func TestShouldRaiseIOErrOnUnreadableFile(t *testing.T) {
@@ -184,6 +186,7 @@ func TestShouldValidateConfigurationWithEnvSecrets(t *testing.T) {
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD_FILE", "./test_resources/example_secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", "./test_resources/example_secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", "./test_resources/example_secret"))
+ assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_ENCRYPTION_KEY_FILE", "./test_resources/example_secret"))
val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
@@ -196,6 +199,7 @@ func TestShouldValidateConfigurationWithEnvSecrets(t *testing.T) {
assert.Equal(t, "example_secret value", config.Session.Secret)
assert.Equal(t, "example_secret value", config.AuthenticationBackend.LDAP.Password)
assert.Equal(t, "example_secret value", config.Storage.MySQL.Password)
+ assert.Equal(t, "example_secret value", config.Storage.EncryptionKey)
}
func TestShouldValidateAndRaiseErrorsOnBadConfiguration(t *testing.T) {
@@ -275,6 +279,7 @@ func testReset() {
testUnsetEnvName("PORT")
testUnsetEnvName("IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY")
testUnsetEnvName("IDENTITY_PROVIDERS_OIDC_HMAC_SECRET")
+ testUnsetEnvName("STORAGE_ENCRYPTION_KEY")
}
func testUnsetEnvName(name string) {
diff --git a/internal/configuration/schema/storage.go b/internal/configuration/schema/storage.go
index ecb2a65ca..831872eb6 100644
--- a/internal/configuration/schema/storage.go
+++ b/internal/configuration/schema/storage.go
@@ -33,6 +33,8 @@ type StorageConfiguration struct {
Local *LocalStorageConfiguration `koanf:"local"`
MySQL *MySQLStorageConfiguration `koanf:"mysql"`
PostgreSQL *PostgreSQLStorageConfiguration `koanf:"postgres"`
+
+ EncryptionKey string `koanf:"encryption_key"`
}
// DefaultPostgreSQLStorageConfiguration represents the default PostgreSQL configuration.
diff --git a/internal/configuration/validator/configuration_test.go b/internal/configuration/validator/configuration_test.go
index 39796abfd..6db192c69 100644
--- a/internal/configuration/validator/configuration_test.go
+++ b/internal/configuration/validator/configuration_test.go
@@ -28,6 +28,7 @@ func newDefaultConfig() schema.Configuration {
Name: "authelia_session",
Secret: "secret",
}
+ config.Storage.EncryptionKey = testEncryptionKey
config.Storage.Local = &schema.LocalStorageConfiguration{
Path: "abc",
}
diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go
index b56819e1c..67f6bef9b 100644
--- a/internal/configuration/validator/const.go
+++ b/internal/configuration/validator/const.go
@@ -41,6 +41,7 @@ const (
testModeDisabled = "disable"
testTLSCert = "/tmp/cert.pem"
testTLSKey = "/tmp/key.pem"
+ testEncryptionKey = "a_not_so_secure_encryption_key"
)
// Notifier Error constants.
@@ -206,6 +207,8 @@ var ValidKeys = []string{
"session.redis.timeouts.read",
"session.redis.timeouts.write",
+ "storage.encryption_key",
+
// Local Storage Keys.
"storage.local.path",
diff --git a/internal/configuration/validator/storage.go b/internal/configuration/validator/storage.go
index 8c303d0a1..b4ab54a2c 100644
--- a/internal/configuration/validator/storage.go
+++ b/internal/configuration/validator/storage.go
@@ -20,6 +20,12 @@ func ValidateStorage(configuration schema.StorageConfiguration, validator *schem
case configuration.Local != nil:
validateLocalStorageConfiguration(configuration.Local, validator)
}
+
+ if configuration.EncryptionKey == "" {
+ validator.Push(errors.New("the configuration option storage.encryption_key must be provided"))
+ } else if len(configuration.EncryptionKey) < 20 {
+ validator.Push(errors.New("the configuration option storage.encryption_key must be 20 characters or longer"))
+ }
}
func validateMySQLConfiguration(configuration *schema.SQLStorageConfiguration, validator *schema.StructValidator) {
diff --git a/internal/configuration/validator/storage_test.go b/internal/configuration/validator/storage_test.go
index fcec6a836..927d93699 100644
--- a/internal/configuration/validator/storage_test.go
+++ b/internal/configuration/validator/storage_test.go
@@ -16,6 +16,7 @@ type StorageSuite struct {
func (suite *StorageSuite) SetupTest() {
suite.validator = schema.NewStructValidator()
+ suite.configuration.EncryptionKey = testEncryptionKey
suite.configuration.Local = &schema.LocalStorageConfiguration{
Path: "/this/is/a/path",
}
@@ -106,6 +107,26 @@ func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeValid() {
suite.Assert().EqualError(suite.validator.Errors()[0], "SSL mode must be 'disable', 'require', 'verify-ca', or 'verify-full'")
}
+func (suite *StorageSuite) TestShouldRaiseErrorOnNoEncryptionKey() {
+ suite.configuration.EncryptionKey = ""
+
+ ValidateStorage(suite.configuration, suite.validator)
+
+ suite.Assert().False(suite.validator.HasWarnings())
+ suite.Require().Len(suite.validator.Errors(), 1)
+ suite.Assert().EqualError(suite.validator.Errors()[0], "the configuration option storage.encryption_key must be provided")
+}
+
+func (suite *StorageSuite) TestShouldRaiseErrorOnShortEncryptionKey() {
+ suite.configuration.EncryptionKey = "abc"
+
+ ValidateStorage(suite.configuration, suite.validator)
+
+ suite.Assert().False(suite.validator.HasWarnings())
+ suite.Require().Len(suite.validator.Errors(), 1)
+ suite.Assert().EqualError(suite.validator.Errors()[0], "the configuration option storage.encryption_key must be 20 characters or longer")
+}
+
func TestShouldRunStorageSuite(t *testing.T) {
suite.Run(t, new(StorageSuite))
}
diff --git a/internal/handlers/handler_register_totp.go b/internal/handlers/handler_register_totp.go
index cdd88305d..81353851d 100644
--- a/internal/handlers/handler_register_totp.go
+++ b/internal/handlers/handler_register_totp.go
@@ -59,7 +59,7 @@ func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username strin
Username: username,
Algorithm: otpAlgoToString(algorithm),
Digits: 6,
- Secret: key.Secret(),
+ Secret: []byte(key.Secret()),
Period: key.Period(),
}
diff --git a/internal/handlers/handler_sign_totp_test.go b/internal/handlers/handler_sign_totp_test.go
index 426c314df..7867e606d 100644
--- a/internal/handlers/handler_sign_totp_test.go
+++ b/internal/handlers/handler_sign_totp_test.go
@@ -38,7 +38,7 @@ func (s *HandlerSignTOTPSuite) TearDownTest() {
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
- config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: "secret", Period: 30, Algorithm: "SHA1"}
+ config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
@@ -65,7 +65,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
- config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: "secret", Period: 30, Algorithm: "SHA1"}
+ config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
@@ -88,7 +88,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
- config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: "secret", Period: 30, Algorithm: "SHA1"}
+ config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
@@ -116,10 +116,10 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
- Return(&models.TOTPConfiguration{Secret: "secret"}, nil)
+ Return(&models.TOTPConfiguration{Secret: []byte("secret")}, nil)
verifier.EXPECT().
- Verify(gomock.Eq(&models.TOTPConfiguration{Secret: "secret"}), gomock.Eq("abc")).
+ Verify(gomock.Eq(&models.TOTPConfiguration{Secret: []byte("secret")}), gomock.Eq("abc")).
Return(true, nil)
bodyBytes, err := json.Marshal(signTOTPRequestBody{
@@ -136,7 +136,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFixation() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
- config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: "secret", Period: 30, Algorithm: "SHA1"}
+ config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
s.mock.StorageProviderMock.EXPECT().
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
diff --git a/internal/handlers/totp.go b/internal/handlers/totp.go
index 510292d68..817b7ab8e 100644
--- a/internal/handlers/totp.go
+++ b/internal/handlers/totp.go
@@ -34,7 +34,7 @@ func (tv *TOTPVerifierImpl) Verify(config *models.TOTPConfiguration, token strin
Algorithm: otpStringToAlgo(config.Algorithm),
}
- return totp.ValidateCustom(token, config.Secret, time.Now().UTC(), opts)
+ return totp.ValidateCustom(token, string(config.Secret), time.Now().UTC(), opts)
}
func otpAlgoToString(algorithm otp.Algorithm) (out string) {
diff --git a/internal/models/model_totp_configuration.go b/internal/models/model_totp_configuration.go
index b274689dc..decb361d2 100644
--- a/internal/models/model_totp_configuration.go
+++ b/internal/models/model_totp_configuration.go
@@ -7,5 +7,5 @@ type TOTPConfiguration struct {
Algorithm string `db:"algorithm"`
Digits int `db:"digits"`
Period uint64 `db:"totp_period"`
- Secret string `db:"secret"`
+ Secret []byte `db:"secret"`
}
diff --git a/internal/storage/const.go b/internal/storage/const.go
index c0cbd73d8..ad2e2ed22 100644
--- a/internal/storage/const.go
+++ b/internal/storage/const.go
@@ -12,10 +12,15 @@ const (
tableDUODevices = "duo_devices"
tableAuthenticationLogs = "authentication_logs"
tableMigrations = "migrations"
+ tableEncryption = "encryption"
tablePrefixBackup = "_bkp_"
)
+const (
+ encryptionNameCheck = "check"
+)
+
// WARNING: Do not change/remove these consts. They are used for Pre1 migrations.
const (
tablePre1TOTPSecrets = "totp_secrets"
diff --git a/internal/storage/errors.go b/internal/storage/errors.go
index d84a2bd66..2b26f6c7c 100644
--- a/internal/storage/errors.go
+++ b/internal/storage/errors.go
@@ -8,17 +8,31 @@ var (
// ErrNoU2FDeviceHandle error thrown when no U2F device handle has been found in DB.
ErrNoU2FDeviceHandle = errors.New("no U2F device handle found")
+ // ErrNoAuthenticationLogs error thrown when no matching authentication logs hve been found in DB.
+ ErrNoAuthenticationLogs = errors.New("no matching authentication logs found")
+
// ErrNoTOTPSecret error thrown when no TOTP secret has been found in DB.
ErrNoTOTPSecret = errors.New("no TOTP secret registered")
// ErrNoAvailableMigrations is returned when no available migrations can be found.
ErrNoAvailableMigrations = errors.New("no available migrations")
+ // ErrMigrateCurrentVersionSameAsTarget is returned when the target version is the same as the current.
+ ErrMigrateCurrentVersionSameAsTarget = errors.New("current version is same as migration target, no action being taken")
+
// ErrSchemaAlreadyUpToDate is returned when the schema is already up to date.
ErrSchemaAlreadyUpToDate = errors.New("schema already up to date")
// ErrNoMigrationsFound is returned when no migrations were found.
ErrNoMigrationsFound = errors.New("no schema migrations found")
+
+ // ErrSchemaEncryptionVersionUnsupported is returned when the schema is checked if the encryption key is valid for
+ // the database but the schema doesn't support encryption.
+ ErrSchemaEncryptionVersionUnsupported = errors.New("schema version doesn't support encryption")
+
+ // ErrSchemaEncryptionInvalidKey is returned when the schema is checked if the encryption key is valid for
+ // the database but the key doesn't appear to be valid.
+ ErrSchemaEncryptionInvalidKey = errors.New("the encryption key is not valid against the schema check value")
)
// Error formats for the storage provider.
diff --git a/internal/storage/migrations.go b/internal/storage/migrations.go
index 4a8427abe..f72225432 100644
--- a/internal/storage/migrations.go
+++ b/internal/storage/migrations.go
@@ -85,7 +85,7 @@ func loadMigration(providerName string, version int, up bool) (migration *Schema
// this indicates the database zero state.
func loadMigrations(providerName string, prior, target int) (migrations []SchemaMigration, err error) {
if prior == target && (prior != -1 || target != -1) {
- return nil, errors.New("cannot migrate to the same version as prior")
+ return nil, ErrMigrateCurrentVersionSameAsTarget
}
entries, err := migrationsFS.ReadDir("migrations")
diff --git a/internal/storage/migrations/V0001.Initial_Schema.all.down.sql b/internal/storage/migrations/V0001.Initial_Schema.all.down.sql
index 1fef4a536..fdee524ce 100644
--- a/internal/storage/migrations/V0001.Initial_Schema.all.down.sql
+++ b/internal/storage/migrations/V0001.Initial_Schema.all.down.sql
@@ -4,3 +4,4 @@ DROP TABLE IF EXISTS totp_configurations;
DROP TABLE IF EXISTS u2f_devices;
DROP TABLE IF EXISTS user_preferences;
DROP TABLE IF EXISTS migrations;
+DROP TABLE IF EXISTS encryption;
\ No newline at end of file
diff --git a/internal/storage/migrations/V0001.Initial_Schema.mysql.up.sql b/internal/storage/migrations/V0001.Initial_Schema.mysql.up.sql
index 858959b5b..e7b1ca848 100644
--- a/internal/storage/migrations/V0001.Initial_Schema.mysql.up.sql
+++ b/internal/storage/migrations/V0001.Initial_Schema.mysql.up.sql
@@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS totp_configurations (
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER NOT NULL DEFAULT 6,
totp_period INTEGER NOT NULL DEFAULT 30,
- secret VARCHAR(64) NOT NULL,
+ secret BLOB NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY (username)
);
@@ -53,3 +53,11 @@ CREATE TABLE IF NOT EXISTS migrations (
application_version VARCHAR(128) NOT NULL,
PRIMARY KEY (id)
);
+
+CREATE TABLE IF NOT EXISTS encryption (
+ id INTEGER AUTO_INCREMENT,
+ name VARCHAR(100),
+ value BLOB NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY (name)
+);
diff --git a/internal/storage/migrations/V0001.Initial_Schema.postgres.up.sql b/internal/storage/migrations/V0001.Initial_Schema.postgres.up.sql
index ade06ccfb..467368de3 100644
--- a/internal/storage/migrations/V0001.Initial_Schema.postgres.up.sql
+++ b/internal/storage/migrations/V0001.Initial_Schema.postgres.up.sql
@@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS totp_configurations (
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER NOT NULL DEFAULT 6,
totp_period INTEGER NOT NULL DEFAULT 30,
- secret VARCHAR(64) NOT NULL,
+ secret BYTEA NOT NULL,
PRIMARY KEY (id),
UNIQUE (username)
);
@@ -53,3 +53,11 @@ CREATE TABLE IF NOT EXISTS migrations (
application_version VARCHAR(128) NOT NULL,
PRIMARY KEY (id)
);
+
+CREATE TABLE IF NOT EXISTS encryption (
+ id SERIAL,
+ name VARCHAR(100),
+ value BYTEA NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE (name)
+);
\ No newline at end of file
diff --git a/internal/storage/migrations/V0001.Initial_Schema.sqlite.up.sql b/internal/storage/migrations/V0001.Initial_Schema.sqlite.up.sql
index 139d45853..2a0b5e6bd 100644
--- a/internal/storage/migrations/V0001.Initial_Schema.sqlite.up.sql
+++ b/internal/storage/migrations/V0001.Initial_Schema.sqlite.up.sql
@@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS totp_configurations (
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
digits INTEGER(1) NOT NULL DEFAULT 6,
totp_period INTEGER NOT NULL DEFAULT 30,
- secret VARCHAR(64) NOT NULL,
+ secret BLOB NOT NULL,
PRIMARY KEY (id),
UNIQUE (username)
);
@@ -52,3 +52,11 @@ CREATE TABLE IF NOT EXISTS migrations (
application_version VARCHAR(128) NOT NULL,
PRIMARY KEY (id)
);
+
+CREATE TABLE IF NOT EXISTS encryption (
+ id INTEGER,
+ name VARCHAR(100),
+ value BLOB NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE (name)
+);
diff --git a/internal/storage/provider.go b/internal/storage/provider.go
index d95881253..1e9ff421a 100644
--- a/internal/storage/provider.go
+++ b/internal/storage/provider.go
@@ -24,6 +24,8 @@ type Provider interface {
SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error)
DeleteTOTPConfiguration(ctx context.Context, username string) (err error)
LoadTOTPConfiguration(ctx context.Context, username string) (config *models.TOTPConfiguration, err error)
+ LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []models.TOTPConfiguration, err error)
+ UpdateTOTPConfigurationSecret(ctx context.Context, config models.TOTPConfiguration) (err error)
SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error)
LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error)
@@ -33,9 +35,14 @@ type Provider interface {
SchemaMigrate(ctx context.Context, up bool, version int) (err error)
SchemaMigrationHistory(ctx context.Context) (migrations []models.Migration, err error)
+ SchemaEncryptionChangeKey(ctx context.Context, encryptionKey string) (err error)
+ SchemaEncryptionCheckKey(ctx context.Context, verbose bool) (err error)
+
SchemaLatestVersion() (version int, err error)
SchemaMigrationsUp(ctx context.Context, version int) (migrations []SchemaMigration, err error)
SchemaMigrationsDown(ctx context.Context, version int) (migrations []SchemaMigration, err error)
+
+ Close() (err error)
}
// RegulatorProvider is an interface providing storage capabilities for persisting any kind of data related to the regulator.
diff --git a/internal/storage/provider_mock.go b/internal/storage/provider_mock.go
index d9a773ab9..4c3fb681f 100644
--- a/internal/storage/provider_mock.go
+++ b/internal/storage/provider_mock.go
@@ -4,13 +4,13 @@
package storage
import (
- context "context"
- reflect "reflect"
- time "time"
+ "context"
+ "reflect"
+ "time"
- gomock "github.com/golang/mock/gomock"
+ "github.com/golang/mock/gomock"
- models "github.com/authelia/authelia/v4/internal/models"
+ "github.com/authelia/authelia/v4/internal/models"
)
// MockProvider is a mock of Provider interface.
@@ -50,6 +50,20 @@ func (mr *MockProviderMockRecorder) AppendAuthenticationLog(arg0, arg1 interface
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendAuthenticationLog", reflect.TypeOf((*MockProvider)(nil).AppendAuthenticationLog), arg0, arg1)
}
+// Close mocks base method.
+func (m *MockProvider) Close() error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Close")
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Close indicates an expected call of Close.
+func (mr *MockProviderMockRecorder) Close() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockProvider)(nil).Close))
+}
+
// DeleteTOTPConfiguration mocks base method.
func (m *MockProvider) DeleteTOTPConfiguration(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
@@ -124,6 +138,21 @@ func (mr *MockProviderMockRecorder) LoadTOTPConfiguration(arg0, arg1 interface{}
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadTOTPConfiguration", reflect.TypeOf((*MockProvider)(nil).LoadTOTPConfiguration), arg0, arg1)
}
+// LoadTOTPConfigurations mocks base method.
+func (m *MockProvider) LoadTOTPConfigurations(arg0 context.Context, arg1, arg2 int) ([]models.TOTPConfiguration, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "LoadTOTPConfigurations", arg0, arg1, arg2)
+ ret0, _ := ret[0].([]models.TOTPConfiguration)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// LoadTOTPConfigurations indicates an expected call of LoadTOTPConfigurations.
+func (mr *MockProviderMockRecorder) LoadTOTPConfigurations(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadTOTPConfigurations", reflect.TypeOf((*MockProvider)(nil).LoadTOTPConfigurations), arg0, arg1, arg2)
+}
+
// LoadU2FDevice mocks base method.
func (m *MockProvider) LoadU2FDevice(arg0 context.Context, arg1 string) (*models.U2FDevice, error) {
m.ctrl.T.Helper()
@@ -224,6 +253,34 @@ func (mr *MockProviderMockRecorder) SaveU2FDevice(arg0, arg1 interface{}) *gomoc
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveU2FDevice", reflect.TypeOf((*MockProvider)(nil).SaveU2FDevice), arg0, arg1)
}
+// SchemaEncryptionChangeKey mocks base method.
+func (m *MockProvider) SchemaEncryptionChangeKey(arg0 context.Context, arg1 string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SchemaEncryptionChangeKey", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// SchemaEncryptionChangeKey indicates an expected call of SchemaEncryptionChangeKey.
+func (mr *MockProviderMockRecorder) SchemaEncryptionChangeKey(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaEncryptionChangeKey", reflect.TypeOf((*MockProvider)(nil).SchemaEncryptionChangeKey), arg0, arg1)
+}
+
+// SchemaEncryptionCheckKey mocks base method.
+func (m *MockProvider) SchemaEncryptionCheckKey(arg0 context.Context, arg1 bool) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SchemaEncryptionCheckKey", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// SchemaEncryptionCheckKey indicates an expected call of SchemaEncryptionCheckKey.
+func (mr *MockProviderMockRecorder) SchemaEncryptionCheckKey(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaEncryptionCheckKey", reflect.TypeOf((*MockProvider)(nil).SchemaEncryptionCheckKey), arg0, arg1)
+}
+
// SchemaLatestVersion mocks base method.
func (m *MockProvider) SchemaLatestVersion() (int, error) {
m.ctrl.T.Helper()
@@ -341,3 +398,17 @@ func (mr *MockProviderMockRecorder) StartupCheck() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartupCheck", reflect.TypeOf((*MockProvider)(nil).StartupCheck))
}
+
+// UpdateTOTPConfigurationSecret mocks base method.
+func (m *MockProvider) UpdateTOTPConfigurationSecret(arg0 context.Context, arg1 models.TOTPConfiguration) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpdateTOTPConfigurationSecret", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// UpdateTOTPConfigurationSecret indicates an expected call of UpdateTOTPConfigurationSecret.
+func (mr *MockProviderMockRecorder) UpdateTOTPConfigurationSecret(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTOTPConfigurationSecret", reflect.TypeOf((*MockProvider)(nil).UpdateTOTPConfigurationSecret), arg0, arg1)
+}
diff --git a/internal/storage/sql_provider.go b/internal/storage/sql_provider.go
index f136e4c6e..9ff5dad2b 100644
--- a/internal/storage/sql_provider.go
+++ b/internal/storage/sql_provider.go
@@ -2,6 +2,7 @@ package storage
import (
"context"
+ "crypto/sha256"
"database/sql"
"errors"
"fmt"
@@ -16,15 +17,16 @@ import (
)
// NewSQLProvider generates a generic SQLProvider to be used with other SQL provider NewUp's.
-func NewSQLProvider(name, driverName, dataSourceName string) (provider SQLProvider) {
+func NewSQLProvider(name, driverName, dataSourceName, encryptionKey string) (provider SQLProvider) {
db, err := sqlx.Open(driverName, dataSourceName)
provider = SQLProvider{
+ db: db,
+ key: sha256.Sum256([]byte(encryptionKey)),
name: name,
driverName: driverName,
- db: db,
- log: logging.Logger(),
errOpen: err,
+ log: logging.Logger(),
sqlInsertAuthenticationAttempt: fmt.Sprintf(queryFmtInsertAuthenticationLogEntry, tableAuthenticationLogs),
sqlSelectAuthenticationAttemptsByUsername: fmt.Sprintf(queryFmtSelect1FAAuthenticationLogEntryByUsername, tableAuthenticationLogs),
@@ -33,9 +35,13 @@ func NewSQLProvider(name, driverName, dataSourceName string) (provider SQLProvid
sqlDeleteIdentityVerification: fmt.Sprintf(queryFmtDeleteIdentityVerification, tableIdentityVerification),
sqlSelectExistsIdentityVerification: fmt.Sprintf(queryFmtSelectExistsIdentityVerification, tableIdentityVerification),
- sqlUpsertTOTPConfig: fmt.Sprintf(queryFmtUpsertTOTPConfiguration, tableTOTPConfigurations),
- sqlDeleteTOTPConfig: fmt.Sprintf(queryFmtDeleteTOTPConfiguration, tableTOTPConfigurations),
- sqlSelectTOTPConfig: fmt.Sprintf(queryFmtSelectTOTPConfiguration, tableTOTPConfigurations),
+ sqlUpsertTOTPConfig: fmt.Sprintf(queryFmtUpsertTOTPConfiguration, tableTOTPConfigurations),
+ sqlDeleteTOTPConfig: fmt.Sprintf(queryFmtDeleteTOTPConfiguration, tableTOTPConfigurations),
+ sqlSelectTOTPConfig: fmt.Sprintf(queryFmtSelectTOTPConfiguration, tableTOTPConfigurations),
+ sqlSelectTOTPConfigs: fmt.Sprintf(queryFmtSelectTOTPConfigurations, tableTOTPConfigurations),
+
+ sqlUpdateTOTPConfigSecret: fmt.Sprintf(queryFmtUpdateTOTPConfigurationSecret, tableTOTPConfigurations),
+ sqlUpdateTOTPConfigSecretByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigurationSecretByUsername, tableTOTPConfigurations),
sqlUpsertU2FDevice: fmt.Sprintf(queryFmtUpsertU2FDevice, tableU2FDevices),
sqlSelectU2FDevice: fmt.Sprintf(queryFmtSelectU2FDevice, tableU2FDevices),
@@ -48,20 +54,29 @@ func NewSQLProvider(name, driverName, dataSourceName string) (provider SQLProvid
sqlSelectMigrations: fmt.Sprintf(queryFmtSelectMigrations, tableMigrations),
sqlSelectLatestMigration: fmt.Sprintf(queryFmtSelectLatestMigration, tableMigrations),
+ sqlUpsertEncryptionValue: fmt.Sprintf(queryFmtUpsertEncryptionValue, tableEncryption),
+ sqlSelectEncryptionValue: fmt.Sprintf(queryFmtSelectEncryptionValue, tableEncryption),
+
sqlFmtRenameTable: queryFmtRenameTable,
}
+ key := sha256.Sum256([]byte(encryptionKey))
+
+ provider.key = key
+
return provider
}
// SQLProvider is a storage provider persisting data in a SQL database.
type SQLProvider struct {
db *sqlx.DB
- log *logrus.Logger
+ key [32]byte
name string
driverName string
errOpen error
+ log *logrus.Logger
+
// Table: authentication_logs.
sqlInsertAuthenticationAttempt string
sqlSelectAuthenticationAttemptsByUsername string
@@ -72,9 +87,13 @@ type SQLProvider struct {
sqlSelectExistsIdentityVerification string
// Table: totp_configurations.
- sqlUpsertTOTPConfig string
- sqlDeleteTOTPConfig string
- sqlSelectTOTPConfig string
+ sqlUpsertTOTPConfig string
+ sqlDeleteTOTPConfig string
+ sqlSelectTOTPConfig string
+ sqlSelectTOTPConfigs string
+
+ sqlUpdateTOTPConfigSecret string
+ sqlUpdateTOTPConfigSecretByUsername string
// Table: u2f_devices.
sqlUpsertU2FDevice string
@@ -90,21 +109,29 @@ type SQLProvider struct {
sqlSelectMigrations string
sqlSelectLatestMigration string
+ // Table: encryption.
+ sqlUpsertEncryptionValue string
+ sqlSelectEncryptionValue string
+
// Utility.
sqlSelectExistingTables string
sqlFmtRenameTable string
}
+// Close the underlying database connection.
+func (p *SQLProvider) Close() (err error) {
+ return p.db.Close()
+}
+
// StartupCheck implements the provider startup check interface.
func (p *SQLProvider) StartupCheck() (err error) {
if p.errOpen != nil {
- return p.errOpen
+ return fmt.Errorf("error opening database: %w", p.errOpen)
}
// TODO: Decide if this is needed, or if it should be configurable.
for i := 0; i < 19; i++ {
- err = p.db.Ping()
- if err == nil {
+ if err = p.db.Ping(); err == nil {
break
}
@@ -112,13 +139,17 @@ func (p *SQLProvider) StartupCheck() (err error) {
}
if err != nil {
- return err
+ return fmt.Errorf("error pinging database: %w", err)
}
p.log.Infof("Storage schema is being checked for updates")
ctx := context.Background()
+ if err = p.SchemaEncryptionCheckKey(ctx, false); err != nil && !errors.Is(err, ErrSchemaEncryptionVersionUnsupported) {
+ return err
+ }
+
err = p.SchemaMigrate(ctx, true, SchemaLatest)
switch err {
@@ -128,7 +159,7 @@ func (p *SQLProvider) StartupCheck() (err error) {
case nil:
return nil
default:
- return err
+ return fmt.Errorf("error during schema migrate: %w", err)
}
}
@@ -143,13 +174,13 @@ func (p *SQLProvider) SavePreferred2FAMethod(ctx context.Context, username strin
func (p *SQLProvider) LoadPreferred2FAMethod(ctx context.Context, username string) (method string, err error) {
err = p.db.GetContext(ctx, &method, p.sqlSelectPreferred2FAMethod, username)
- switch err {
- case sql.ErrNoRows:
+ switch {
+ case err == nil:
+ return method, nil
+ case errors.Is(err, sql.ErrNoRows):
return "", nil
- case nil:
- return method, err
default:
- return "", err
+ return "", fmt.Errorf("error selecting preferred two factor method for user '%s': %w", username, err)
}
}
@@ -161,120 +192,98 @@ func (p *SQLProvider) LoadUserInfo(ctx context.Context, username string) (info m
case err == nil:
return info, nil
case errors.Is(err, sql.ErrNoRows):
- _, err = p.db.ExecContext(ctx, p.sqlUpsertPreferred2FAMethod, username, authentication.PossibleMethods[0])
- if err != nil {
- return models.UserInfo{}, err
+ if _, err = p.db.ExecContext(ctx, p.sqlUpsertPreferred2FAMethod, username, authentication.PossibleMethods[0]); err != nil {
+ return models.UserInfo{}, fmt.Errorf("error upserting preferred two factor method while selecting user info for user '%s': %w", username, err)
}
- err = p.db.GetContext(ctx, &info, p.sqlSelectUserInfo, username, username, username)
- if err != nil {
- return models.UserInfo{}, err
+ if err = p.db.GetContext(ctx, &info, p.sqlSelectUserInfo, username, username, username); err != nil {
+ return models.UserInfo{}, fmt.Errorf("error selecting user info for user '%s': %w", username, err)
}
return info, nil
default:
- return models.UserInfo{}, err
+ return models.UserInfo{}, fmt.Errorf("error selecting user info for user '%s': %w", username, err)
}
}
// SaveIdentityVerification save an identity verification record to the database.
func (p *SQLProvider) SaveIdentityVerification(ctx context.Context, verification models.IdentityVerification) (err error) {
- _, err = p.db.ExecContext(ctx, p.sqlInsertIdentityVerification, verification.Token)
+ if _, err = p.db.ExecContext(ctx, p.sqlInsertIdentityVerification, verification.Token); err != nil {
+ return fmt.Errorf("error inserting identity verification: %w", err)
+ }
- return err
+ return nil
}
// RemoveIdentityVerification remove an identity verification record from the database.
func (p *SQLProvider) RemoveIdentityVerification(ctx context.Context, token string) (err error) {
- _, err = p.db.ExecContext(ctx, p.sqlDeleteIdentityVerification, token)
+ if _, err = p.db.ExecContext(ctx, p.sqlDeleteIdentityVerification, token); err != nil {
+ return fmt.Errorf("error updating identity verification: %w", err)
+ }
- return err
+ return nil
}
// FindIdentityVerification checks if an identity verification record is in the database and active.
-func (p *SQLProvider) FindIdentityVerification(ctx context.Context, jti string) (found bool, err error) {
- err = p.db.GetContext(ctx, &found, p.sqlSelectExistsIdentityVerification, jti)
- if err != nil {
- return false, err
+func (p *SQLProvider) FindIdentityVerification(ctx context.Context, token string) (found bool, err error) {
+ if err = p.db.GetContext(ctx, &found, p.sqlSelectExistsIdentityVerification, token); err != nil {
+ return false, fmt.Errorf("error selecting identity verification exists: %w", err)
}
return found, nil
}
-// SaveTOTPConfiguration save a TOTP config of a given user in the database.
+// SaveTOTPConfiguration save a TOTP configuration of a given user in the database.
func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error) {
- // TODO: Encrypt config.Secret here.
- _, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig,
- config.Username,
- config.Algorithm,
- config.Digits,
- config.Period,
- config.Secret,
- )
+ if config.Secret, err = p.encrypt(config.Secret); err != nil {
+ return fmt.Errorf("error encrypting the TOTP configuration secret: %v", err)
+ }
- return err
+ if _, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig,
+ config.Username, config.Algorithm, config.Digits, config.Period, config.Secret); err != nil {
+ return fmt.Errorf("error upserting TOTP configuration: %w", err)
+ }
+
+ return nil
}
-// DeleteTOTPConfiguration delete a TOTP secret from the database given a username.
+// DeleteTOTPConfiguration delete a TOTP configuration from the database given a username.
func (p *SQLProvider) DeleteTOTPConfiguration(ctx context.Context, username string) (err error) {
- _, err = p.db.ExecContext(ctx, p.sqlDeleteTOTPConfig, username)
+ if _, err = p.db.ExecContext(ctx, p.sqlDeleteTOTPConfig, username); err != nil {
+ return fmt.Errorf("error deleting TOTP configuration: %w", err)
+ }
- return err
+ return nil
}
-// LoadTOTPConfiguration load a TOTP secret given a username from the database.
+// LoadTOTPConfiguration load a TOTP configuration given a username from the database.
func (p *SQLProvider) LoadTOTPConfiguration(ctx context.Context, username string) (config *models.TOTPConfiguration, err error) {
config = &models.TOTPConfiguration{}
- err = p.db.QueryRowxContext(ctx, p.sqlSelectTOTPConfig, username).StructScan(config)
- if err != nil {
- if err == sql.ErrNoRows {
+ if err = p.db.QueryRowxContext(ctx, p.sqlSelectTOTPConfig, username).StructScan(config); err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoTOTPSecret
}
- return nil, err
+ return nil, fmt.Errorf("error selecting TOTP configuration: %w", err)
+ }
+
+ if config.Secret, err = p.decrypt(config.Secret); err != nil {
+ return nil, fmt.Errorf("error decrypting the TOTP secret: %v", err)
}
- // TODO: Decrypt config.Secret here.
return config, nil
}
-// SaveU2FDevice saves a registered U2F device.
-func (p *SQLProvider) SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error) {
- _, err = p.db.ExecContext(ctx, p.sqlUpsertU2FDevice, device.Username, device.KeyHandle, device.PublicKey)
-
- return err
-}
-
-// LoadU2FDevice loads a U2F device registration for a given username.
-func (p *SQLProvider) LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error) {
- device = &models.U2FDevice{
- Username: username,
- }
-
- err = p.db.GetContext(ctx, device, p.sqlSelectU2FDevice, username)
+// LoadTOTPConfigurations load a set of TOTP configurations.
+func (p *SQLProvider) LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []models.TOTPConfiguration, err error) {
+ rows, err := p.db.QueryxContext(ctx, p.sqlSelectTOTPConfigs, limit, limit*page)
if err != nil {
- if err == sql.ErrNoRows {
- return nil, ErrNoU2FDeviceHandle
+ if errors.Is(err, sql.ErrNoRows) {
+ return configs, nil
}
- return nil, err
- }
-
- return device, nil
-}
-
-// AppendAuthenticationLog append a mark to the authentication log.
-func (p *SQLProvider) AppendAuthenticationLog(ctx context.Context, attempt models.AuthenticationAttempt) (err error) {
- _, err = p.db.ExecContext(ctx, p.sqlInsertAuthenticationAttempt, attempt.Time, attempt.Successful, attempt.Username)
- return err
-}
-
-// LoadAuthenticationLogs retrieve the latest failed authentications from the authentication log.
-func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username string, fromDate time.Time, limit, page int) (attempts []models.AuthenticationAttempt, err error) {
- rows, err := p.db.QueryxContext(ctx, p.sqlSelectAuthenticationAttemptsByUsername, fromDate, username, limit, limit*page)
- if err != nil {
- return nil, err
+ return nil, fmt.Errorf("error selecting TOTP configurations: %w", err)
}
defer func() {
@@ -283,13 +292,99 @@ func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username strin
}
}()
+ configs = make([]models.TOTPConfiguration, 0, limit)
+
+ var config models.TOTPConfiguration
+
+ for rows.Next() {
+ if err = rows.StructScan(&config); err != nil {
+ return nil, fmt.Errorf("error scanning TOTP configuration to struct: %w", err)
+ }
+
+ if config.Secret, err = p.decrypt(config.Secret); err != nil {
+ return nil, fmt.Errorf("error decrypting the TOTP secret: %v", err)
+ }
+
+ configs = append(configs, config)
+ }
+
+ return configs, nil
+}
+
+// UpdateTOTPConfigurationSecret updates a TOTP configuration secret.
+func (p *SQLProvider) UpdateTOTPConfigurationSecret(ctx context.Context, config models.TOTPConfiguration) (err error) {
+ switch config.ID {
+ case 0:
+ _, err = p.db.ExecContext(ctx, p.sqlUpdateTOTPConfigSecretByUsername, config.Secret, config.Username)
+ default:
+ _, err = p.db.ExecContext(ctx, p.sqlUpdateTOTPConfigSecret, config.Secret, config.ID)
+ }
+
+ if err != nil {
+ return fmt.Errorf("error updating TOTP configuration secret: %w", err)
+ }
+
+ return nil
+}
+
+// SaveU2FDevice saves a registered U2F device.
+func (p *SQLProvider) SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error) {
+ if _, err = p.db.ExecContext(ctx, p.sqlUpsertU2FDevice, device.Username, device.KeyHandle, device.PublicKey); err != nil {
+ return fmt.Errorf("error upserting U2F device secret: %v", err)
+ }
+
+ return nil
+}
+
+// LoadU2FDevice loads a U2F device registration for a given username.
+func (p *SQLProvider) LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error) {
+ device = &models.U2FDevice{
+ Username: username,
+ }
+
+ if err = p.db.GetContext(ctx, device, p.sqlSelectU2FDevice, username); err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, ErrNoU2FDeviceHandle
+ }
+
+ return nil, fmt.Errorf("error selecting U2F device: %w", err)
+ }
+
+ return device, nil
+}
+
+// AppendAuthenticationLog append a mark to the authentication log.
+func (p *SQLProvider) AppendAuthenticationLog(ctx context.Context, attempt models.AuthenticationAttempt) (err error) {
+ if _, err = p.db.ExecContext(ctx, p.sqlInsertAuthenticationAttempt, attempt.Time, attempt.Successful, attempt.Username); err != nil {
+ return fmt.Errorf("error inserting authentiation attempt: %w", err)
+ }
+
+ return nil
+}
+
+// LoadAuthenticationLogs retrieve the latest failed authentications from the authentication log.
+func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username string, fromDate time.Time, limit, page int) (attempts []models.AuthenticationAttempt, err error) {
+ rows, err := p.db.QueryxContext(ctx, p.sqlSelectAuthenticationAttemptsByUsername, fromDate, username, limit, limit*page)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, ErrNoAuthenticationLogs
+ }
+
+ return nil, fmt.Errorf("error selecting authentication logs: %w", err)
+ }
+
+ defer func() {
+ if err := rows.Close(); err != nil {
+ p.log.Errorf(logFmtErrClosingConn, err)
+ }
+ }()
+
+ var attempt models.AuthenticationAttempt
+
attempts = make([]models.AuthenticationAttempt, 0, limit)
for rows.Next() {
- var attempt models.AuthenticationAttempt
-
- err = rows.StructScan(&attempt)
- if err != nil {
+ if err = rows.StructScan(&attempt); err != nil {
return nil, err
}
diff --git a/internal/storage/sql_provider_backend_mysql.go b/internal/storage/sql_provider_backend_mysql.go
index dfb7ec179..8805aea21 100644
--- a/internal/storage/sql_provider_backend_mysql.go
+++ b/internal/storage/sql_provider_backend_mysql.go
@@ -15,9 +15,9 @@ type MySQLProvider struct {
}
// NewMySQLProvider a MySQL provider.
-func NewMySQLProvider(config schema.MySQLStorageConfiguration) (provider *MySQLProvider) {
+func NewMySQLProvider(config schema.MySQLStorageConfiguration, encryptionKey string) (provider *MySQLProvider) {
provider = &MySQLProvider{
- SQLProvider: NewSQLProvider(providerMySQL, providerMySQL, dataSourceNameMySQL(config)),
+ SQLProvider: NewSQLProvider(providerMySQL, providerMySQL, dataSourceNameMySQL(config), encryptionKey),
}
// All providers have differing SELECT existing table statements.
diff --git a/internal/storage/sql_provider_backend_postgres.go b/internal/storage/sql_provider_backend_postgres.go
index 5a79afe84..8b59367a7 100644
--- a/internal/storage/sql_provider_backend_postgres.go
+++ b/internal/storage/sql_provider_backend_postgres.go
@@ -16,9 +16,9 @@ type PostgreSQLProvider struct {
}
// NewPostgreSQLProvider a PostgreSQL provider.
-func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration) (provider *PostgreSQLProvider) {
+func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration, encryptionKey string) (provider *PostgreSQLProvider) {
provider = &PostgreSQLProvider{
- SQLProvider: NewSQLProvider(providerPostgres, "pgx", dataSourceNamePostgreSQL(config)),
+ SQLProvider: NewSQLProvider(providerPostgres, "pgx", dataSourceNamePostgreSQL(config), encryptionKey),
}
// All providers have differing SELECT existing table statements.
@@ -29,6 +29,7 @@ func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration) (provid
provider.sqlUpsertU2FDevice = fmt.Sprintf(queryFmtPostgresUpsertU2FDevice, tableU2FDevices)
provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtPostgresUpsertTOTPConfiguration, tableTOTPConfigurations)
provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtPostgresUpsertPreferred2FAMethod, tableUserPreferences)
+ provider.sqlUpsertEncryptionValue = fmt.Sprintf(queryFmtPostgresUpsertEncryptionValue, tableEncryption)
// PostgreSQL requires rebinding of any query that contains a '?' placeholder to use the '$#' notation placeholders.
provider.sqlFmtRenameTable = provider.db.Rebind(provider.sqlFmtRenameTable)
@@ -40,10 +41,14 @@ func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration) (provid
provider.sqlSelectTOTPConfig = provider.db.Rebind(provider.sqlSelectTOTPConfig)
provider.sqlUpsertTOTPConfig = provider.db.Rebind(provider.sqlUpsertTOTPConfig)
provider.sqlDeleteTOTPConfig = provider.db.Rebind(provider.sqlDeleteTOTPConfig)
+ provider.sqlSelectTOTPConfigs = provider.db.Rebind(provider.sqlSelectTOTPConfigs)
+ provider.sqlUpdateTOTPConfigSecret = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecret)
+ provider.sqlUpdateTOTPConfigSecretByUsername = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecretByUsername)
provider.sqlSelectU2FDevice = provider.db.Rebind(provider.sqlSelectU2FDevice)
provider.sqlInsertAuthenticationAttempt = provider.db.Rebind(provider.sqlInsertAuthenticationAttempt)
provider.sqlSelectAuthenticationAttemptsByUsername = provider.db.Rebind(provider.sqlSelectAuthenticationAttemptsByUsername)
provider.sqlInsertMigration = provider.db.Rebind(provider.sqlInsertMigration)
+ provider.sqlSelectEncryptionValue = provider.db.Rebind(provider.sqlSelectEncryptionValue)
return provider
}
diff --git a/internal/storage/sql_provider_backend_sqlite.go b/internal/storage/sql_provider_backend_sqlite.go
index b11b82239..f54309b06 100644
--- a/internal/storage/sql_provider_backend_sqlite.go
+++ b/internal/storage/sql_provider_backend_sqlite.go
@@ -10,9 +10,9 @@ type SQLiteProvider struct {
}
// NewSQLiteProvider constructs a SQLite provider.
-func NewSQLiteProvider(path string) (provider *SQLiteProvider) {
+func NewSQLiteProvider(path, encryptionKey string) (provider *SQLiteProvider) {
provider = &SQLiteProvider{
- SQLProvider: NewSQLProvider(providerSQLite, "sqlite3", path),
+ SQLProvider: NewSQLProvider(providerSQLite, "sqlite3", path, encryptionKey),
}
// All providers have differing SELECT existing table statements.
diff --git a/internal/storage/sql_provider_encryption.go b/internal/storage/sql_provider_encryption.go
new file mode 100644
index 000000000..b7b2d1172
--- /dev/null
+++ b/internal/storage/sql_provider_encryption.go
@@ -0,0 +1,185 @@
+package storage
+
+import (
+ "context"
+ "crypto/sha256"
+ "fmt"
+
+ "github.com/google/uuid"
+ "github.com/jmoiron/sqlx"
+
+ "github.com/authelia/authelia/v4/internal/models"
+ "github.com/authelia/authelia/v4/internal/utils"
+)
+
+// SchemaEncryptionChangeKey uses the currently configured key to decrypt values in the database and the key provided
+// by this command to encrypt the values again and update them using a transaction.
+func (p *SQLProvider) SchemaEncryptionChangeKey(ctx context.Context, encryptionKey string) (err error) {
+ tx, err := p.db.Beginx()
+ if err != nil {
+ return fmt.Errorf("error beginning transaction to change encryption key: %w", err)
+ }
+
+ key := sha256.Sum256([]byte(encryptionKey))
+
+ var configs []models.TOTPConfiguration
+
+ for page := 0; true; page++ {
+ if configs, err = p.LoadTOTPConfigurations(ctx, 10, page); err != nil {
+ if rollbackErr := tx.Rollback(); rollbackErr != nil {
+ return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
+ }
+
+ return fmt.Errorf("rollback due to error: %w", err)
+ }
+
+ for _, config := range configs {
+ if config.Secret, err = utils.Encrypt(config.Secret, &key); err != nil {
+ if rollbackErr := tx.Rollback(); rollbackErr != nil {
+ return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
+ }
+
+ return fmt.Errorf("rollback due to error: %w", err)
+ }
+
+ if err = p.UpdateTOTPConfigurationSecret(ctx, config); err != nil {
+ if rollbackErr := tx.Rollback(); rollbackErr != nil {
+ return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
+ }
+
+ return fmt.Errorf("rollback due to error: %w", err)
+ }
+ }
+
+ if len(configs) != 10 {
+ break
+ }
+ }
+
+ if err = p.setNewEncryptionCheckValue(ctx, &key, tx); err != nil {
+ if rollbackErr := tx.Rollback(); rollbackErr != nil {
+ return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
+ }
+
+ return fmt.Errorf("rollback due to error: %w", err)
+ }
+
+ return tx.Commit()
+}
+
+// SchemaEncryptionCheckKey checks the encryption key configured is valid for the database.
+func (p *SQLProvider) SchemaEncryptionCheckKey(ctx context.Context, verbose bool) (err error) {
+ version, err := p.SchemaVersion(ctx)
+ if err != nil {
+ return err
+ }
+
+ if version < 1 {
+ return ErrSchemaEncryptionVersionUnsupported
+ }
+
+ var errs []error
+
+ if _, err = p.getEncryptionValue(ctx, encryptionNameCheck); err != nil {
+ errs = append(errs, ErrSchemaEncryptionInvalidKey)
+ }
+
+ if verbose {
+ var (
+ config models.TOTPConfiguration
+ row int
+ invalid int
+ total int
+ )
+
+ pageSize := 10
+
+ var rows *sqlx.Rows
+
+ for page := 0; true; page++ {
+ if rows, err = p.db.QueryxContext(ctx, p.sqlSelectTOTPConfigs, pageSize, pageSize*page); err != nil {
+ _ = rows.Close()
+
+ return fmt.Errorf("error selecting TOTP configurations: %w", err)
+ }
+
+ row = 0
+
+ for rows.Next() {
+ total++
+ row++
+
+ if err = rows.StructScan(&config); err != nil {
+ _ = rows.Close()
+ return fmt.Errorf("error scanning TOTP configuration to struct: %w", err)
+ }
+
+ if _, err = p.decrypt(config.Secret); err != nil {
+ invalid++
+ }
+ }
+
+ _ = rows.Close()
+
+ if row < pageSize {
+ break
+ }
+ }
+
+ if invalid != 0 {
+ errs = append(errs, fmt.Errorf("%d of %d total TOTP secrets were invalid", invalid, total))
+ }
+ }
+
+ if len(errs) != 0 {
+ for i, e := range errs {
+ if i == 0 {
+ err = e
+
+ continue
+ }
+
+ err = fmt.Errorf("%w, %v", err, e)
+ }
+
+ return err
+ }
+
+ return nil
+}
+
+func (p SQLProvider) encrypt(clearText []byte) (cipherText []byte, err error) {
+ return utils.Encrypt(clearText, &p.key)
+}
+
+func (p SQLProvider) decrypt(cipherText []byte) (clearText []byte, err error) {
+ return utils.Decrypt(cipherText, &p.key)
+}
+
+func (p *SQLProvider) getEncryptionValue(ctx context.Context, name string) (value []byte, err error) {
+ var encryptedValue []byte
+
+ err = p.db.GetContext(ctx, &encryptedValue, p.sqlSelectEncryptionValue, name)
+ if err != nil {
+ return nil, err
+ }
+
+ return p.decrypt(encryptedValue)
+}
+
+func (p *SQLProvider) setNewEncryptionCheckValue(ctx context.Context, key *[32]byte, e sqlx.ExecerContext) (err error) {
+ valueClearText := uuid.New()
+
+ value, err := utils.Encrypt([]byte(valueClearText.String()), key)
+ if err != nil {
+ return err
+ }
+
+ if e != nil {
+ _, err = e.ExecContext(ctx, p.sqlUpsertEncryptionValue, encryptionNameCheck, value)
+ } else {
+ _, err = p.db.ExecContext(ctx, p.sqlUpsertEncryptionValue, encryptionNameCheck, value)
+ }
+
+ return err
+}
diff --git a/internal/storage/sql_provider_queries.go b/internal/storage/sql_provider_queries.go
index f11585d7d..57c2f4687 100644
--- a/internal/storage/sql_provider_queries.go
+++ b/internal/storage/sql_provider_queries.go
@@ -78,6 +78,24 @@ const (
FROM %s
WHERE username = ?;`
+ queryFmtSelectTOTPConfigurations = `
+ SELECT id, username, algorithm, digits, totp_period, secret
+ FROM %s
+ LIMIT ?
+ OFFSET ?;`
+
+ //nolint:gosec // These are not hardcoded credentials it's a query to obtain credentials.
+ queryFmtUpdateTOTPConfigurationSecret = `
+ UPDATE %s
+ SET secret = ?
+ WHERE id = ?;`
+
+ //nolint:gosec // These are not hardcoded credentials it's a query to obtain credentials.
+ queryFmtUpdateTOTPConfigurationSecretByUsername = `
+ UPDATE %s
+ SET secret = ?
+ WHERE username = ?;`
+
queryFmtUpsertTOTPConfiguration = `
REPLACE INTO %s (username, algorithm, digits, totp_period, secret)
VALUES (?, ?, ?, ?, ?);`
@@ -123,3 +141,20 @@ const (
LIMIT ?
OFFSET ?;`
)
+
+const (
+ queryFmtSelectEncryptionValue = `
+ SELECT (value)
+ FROM %s
+ WHERE name = ?`
+
+ queryFmtUpsertEncryptionValue = `
+ REPLACE INTO %s (name, value)
+ VALUES (?, ?);`
+
+ queryFmtPostgresUpsertEncryptionValue = `
+ INSERT INTO %s (name, value)
+ VALUES ($1, $2)
+ ON CONFLICT (name)
+ DO UPDATE SET value=$2;`
+)
diff --git a/internal/storage/sql_provider_schema.go b/internal/storage/sql_provider_schema.go
index 6abb5ff58..fc97c531b 100644
--- a/internal/storage/sql_provider_schema.go
+++ b/internal/storage/sql_provider_schema.go
@@ -63,8 +63,6 @@ func (p *SQLProvider) SchemaVersion(ctx context.Context) (version int, err error
return -1, nil
}
- // TODO: Decide if we want to support external tables.
- // return -2, ErrUnknownSchemaState
return 0, nil
}
@@ -213,7 +211,18 @@ func (p *SQLProvider) schemaMigrateApply(ctx context.Context, migration SchemaMi
return fmt.Errorf(errFmtFailedMigration, migration.Version, migration.Name, err)
}
- // Skip the migration history insertion in a migration to v0.
+ if migration.Version == 1 {
+ // Skip the migration history insertion in a migration to v0.
+ if !migration.Up {
+ return nil
+ }
+
+ // Add the schema encryption value if upgrading to v1.
+ if err = p.setNewEncryptionCheckValue(ctx, &p.key, nil); err != nil {
+ return err
+ }
+ }
+
if migration.Version == 1 && !migration.Up {
return nil
}
diff --git a/internal/storage/sql_provider_schema_pre1.go b/internal/storage/sql_provider_schema_pre1.go
index a8aad4509..a9d40e1a4 100644
--- a/internal/storage/sql_provider_schema_pre1.go
+++ b/internal/storage/sql_provider_schema_pre1.go
@@ -49,6 +49,10 @@ func (p *SQLProvider) schemaMigratePre1To1(ctx context.Context) (err error) {
return fmt.Errorf(errFmtFailedMigration, migration.Version, migration.Name, err)
}
+ if err = p.setNewEncryptionCheckValue(ctx, &p.key, nil); err != nil {
+ return err
+ }
+
if _, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1InsertUserPreferencesFromSelect),
tableUserPreferences, tablePrefixBackup+tableUserPreferences)); err != nil {
return err
@@ -213,8 +217,10 @@ func (p *SQLProvider) schemaMigratePre1To1TOTP(ctx context.Context) (err error)
return err
}
- // TODO: Add encryption migration here.
- encryptedSecret := "encrypted:" + secret
+ encryptedSecret, err := p.encrypt([]byte(secret))
+ if err != nil {
+ return err
+ }
totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: encryptedSecret})
}
@@ -288,6 +294,7 @@ func (p *SQLProvider) schemaMigrate1ToPre1(ctx context.Context) (err error) {
tableDUODevices,
tableUserPreferences,
tableAuthenticationLogs,
+ tableEncryption,
}
if err = p.schemaMigratePre1Rename(ctx, tables, tablesRename); err != nil {
@@ -388,18 +395,22 @@ func (p *SQLProvider) schemaMigrate1ToPre1TOTP(ctx context.Context) (err error)
}()
for rows.Next() {
- var username, encryptedSecret string
+ var (
+ username string
+ secretCipherText []byte
+ )
- err = rows.Scan(&username, &encryptedSecret)
+ err = rows.Scan(&username, &secretCipherText)
if err != nil {
return err
}
- // TODO: Fix.
- // TODO: Add DECRYPTION migration here.
- decryptedSecret := strings.Replace(encryptedSecret, "encrypted:", "", 1)
+ secretClearText, err := p.decrypt(secretCipherText)
+ if err != nil {
+ return err
+ }
- totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: decryptedSecret})
+ totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: secretClearText})
}
for _, config := range totpConfigs {
diff --git a/internal/suites/ActiveDirectory/configuration.yml b/internal/suites/ActiveDirectory/configuration.yml
index 9f8ed8439..a1adb91cd 100644
--- a/internal/suites/ActiveDirectory/configuration.yml
+++ b/internal/suites/ActiveDirectory/configuration.yml
@@ -37,6 +37,7 @@ session:
remember_me_duration: 1y
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite3
diff --git a/internal/suites/BypassAll/configuration.yml b/internal/suites/BypassAll/configuration.yml
index 09caba0f6..25a4c5c8b 100644
--- a/internal/suites/BypassAll/configuration.yml
+++ b/internal/suites/BypassAll/configuration.yml
@@ -26,6 +26,7 @@ session:
remember_me_duration: 1y
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite
diff --git a/internal/suites/CLI/configuration.yml b/internal/suites/CLI/configuration.yml
index d4c33c5ca..de0f31cc6 100644
--- a/internal/suites/CLI/configuration.yml
+++ b/internal/suites/CLI/configuration.yml
@@ -26,8 +26,9 @@ session:
remember_me_duration: 1y
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
- path: /config/db.sqlite
+ path: /tmp/db.sqlite
access_control:
default_policy: bypass
@@ -43,5 +44,5 @@ access_control:
notifier:
filesystem:
- filename: /config/notification.txt
+ filename: /tmp/notification.txt
...
diff --git a/internal/suites/CLI/docker-compose.yml b/internal/suites/CLI/docker-compose.yml
index 5935d4a51..180ba191e 100644
--- a/internal/suites/CLI/docker-compose.yml
+++ b/internal/suites/CLI/docker-compose.yml
@@ -4,6 +4,9 @@ services:
authelia-backend:
volumes:
- './CLI/configuration.yml:/config/configuration.yml:ro'
+ - './CLI/storage.yml:/config/configuration.storage.yml:ro'
- './CLI/users.yml:/config/users.yml'
- './common/ssl:/config/ssl:ro'
+ - '/tmp:/tmp'
+ user: ${USER_ID}:${GROUP_ID}
...
diff --git a/internal/suites/CLI/storage.yml b/internal/suites/CLI/storage.yml
new file mode 100644
index 000000000..ca6381fa9
--- /dev/null
+++ b/internal/suites/CLI/storage.yml
@@ -0,0 +1,6 @@
+---
+storage:
+ encryption_key: a_cli_encryption_key_which_isnt_secure
+ local:
+ path: /tmp/db.sqlite3
+...
diff --git a/internal/suites/Docker/configuration.yml b/internal/suites/Docker/configuration.yml
index 056c63ed9..5ac4aaa5b 100644
--- a/internal/suites/Docker/configuration.yml
+++ b/internal/suites/Docker/configuration.yml
@@ -27,6 +27,7 @@ session:
remember_me_duration: 1y
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite3
diff --git a/internal/suites/DuoPush/configuration.yml b/internal/suites/DuoPush/configuration.yml
index d0881199b..56a7ec8c2 100644
--- a/internal/suites/DuoPush/configuration.yml
+++ b/internal/suites/DuoPush/configuration.yml
@@ -28,6 +28,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite
diff --git a/internal/suites/HAProxy/configuration.yml b/internal/suites/HAProxy/configuration.yml
index 046c181dc..193cae03d 100644
--- a/internal/suites/HAProxy/configuration.yml
+++ b/internal/suites/HAProxy/configuration.yml
@@ -26,6 +26,7 @@ session:
remember_me_duration: 1y
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite
diff --git a/internal/suites/HighAvailability/configuration.yml b/internal/suites/HighAvailability/configuration.yml
index e9cd1a52b..2b0cc5987 100644
--- a/internal/suites/HighAvailability/configuration.yml
+++ b/internal/suites/HighAvailability/configuration.yml
@@ -110,6 +110,7 @@ regulation:
ban_time: 10
storage:
+ encryption_key: a_not_so_secure_encryption_key
mysql:
host: mariadb
port: 3306
diff --git a/internal/suites/LDAP/configuration.yml b/internal/suites/LDAP/configuration.yml
index 432efaf00..4bcd9a307 100644
--- a/internal/suites/LDAP/configuration.yml
+++ b/internal/suites/LDAP/configuration.yml
@@ -41,6 +41,7 @@ session:
remember_me_duration: 1y
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite3
diff --git a/internal/suites/Mariadb/configuration.yml b/internal/suites/Mariadb/configuration.yml
index d2adc36d1..0c406aa1f 100644
--- a/internal/suites/Mariadb/configuration.yml
+++ b/internal/suites/Mariadb/configuration.yml
@@ -28,6 +28,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
+ encryption_key: a_not_so_secure_encryption_key
mysql:
host: mariadb
port: 3306
diff --git a/internal/suites/MySQL/configuration.yml b/internal/suites/MySQL/configuration.yml
index d847cb2f1..f57595375 100644
--- a/internal/suites/MySQL/configuration.yml
+++ b/internal/suites/MySQL/configuration.yml
@@ -29,6 +29,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
+ encryption_key: a_not_so_secure_encryption_key
mysql:
host: mysql
port: 3306
diff --git a/internal/suites/NetworkACL/configuration.yml b/internal/suites/NetworkACL/configuration.yml
index 1c83a54b1..a4a6103d0 100644
--- a/internal/suites/NetworkACL/configuration.yml
+++ b/internal/suites/NetworkACL/configuration.yml
@@ -27,6 +27,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite
diff --git a/internal/suites/OIDC/configuration.yml b/internal/suites/OIDC/configuration.yml
index 8508271de..c63892dc4 100644
--- a/internal/suites/OIDC/configuration.yml
+++ b/internal/suites/OIDC/configuration.yml
@@ -27,6 +27,7 @@ session:
port: 6379
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite
diff --git a/internal/suites/OIDCTraefik/configuration.yml b/internal/suites/OIDCTraefik/configuration.yml
index 3381211fe..cc949e563 100644
--- a/internal/suites/OIDCTraefik/configuration.yml
+++ b/internal/suites/OIDCTraefik/configuration.yml
@@ -27,6 +27,7 @@ session:
port: 6379
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite
diff --git a/internal/suites/OneFactorOnly/configuration.yml b/internal/suites/OneFactorOnly/configuration.yml
index b90513fec..32ead64e6 100644
--- a/internal/suites/OneFactorOnly/configuration.yml
+++ b/internal/suites/OneFactorOnly/configuration.yml
@@ -27,6 +27,7 @@ session:
remember_me_duration: 1y
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite
diff --git a/internal/suites/PathPrefix/configuration.yml b/internal/suites/PathPrefix/configuration.yml
index 72f37c065..b2865affc 100644
--- a/internal/suites/PathPrefix/configuration.yml
+++ b/internal/suites/PathPrefix/configuration.yml
@@ -27,6 +27,7 @@ session:
remember_me_duration: 1y
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite
diff --git a/internal/suites/Postgres/configuration.yml b/internal/suites/Postgres/configuration.yml
index 53ce1e700..a95ffa96c 100644
--- a/internal/suites/Postgres/configuration.yml
+++ b/internal/suites/Postgres/configuration.yml
@@ -28,6 +28,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
+ encryption_key: a_not_so_secure_encryption_key
postgres:
host: postgres
port: 5432
diff --git a/internal/suites/ShortTimeouts/configuration.yml b/internal/suites/ShortTimeouts/configuration.yml
index a091b2c94..0ac5fe3fe 100644
--- a/internal/suites/ShortTimeouts/configuration.yml
+++ b/internal/suites/ShortTimeouts/configuration.yml
@@ -27,6 +27,7 @@ session:
remember_me_duration: 1y
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite
diff --git a/internal/suites/Standalone/configuration.yml b/internal/suites/Standalone/configuration.yml
index 5dec5e49e..4f5e71510 100644
--- a/internal/suites/Standalone/configuration.yml
+++ b/internal/suites/Standalone/configuration.yml
@@ -25,6 +25,7 @@ session:
remember_me_duration: 1y
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /tmp/db.sqlite3
diff --git a/internal/suites/Traefik/configuration.yml b/internal/suites/Traefik/configuration.yml
index d76301826..31c5d75ba 100644
--- a/internal/suites/Traefik/configuration.yml
+++ b/internal/suites/Traefik/configuration.yml
@@ -27,6 +27,7 @@ session:
remember_me_duration: 1y
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite
diff --git a/internal/suites/Traefik2/configuration.yml b/internal/suites/Traefik2/configuration.yml
index 6390464aa..3192c9bec 100644
--- a/internal/suites/Traefik2/configuration.yml
+++ b/internal/suites/Traefik2/configuration.yml
@@ -32,6 +32,7 @@ session:
password: redis-user-password
storage:
+ encryption_key: a_not_so_secure_encryption_key
local:
path: /config/db.sqlite
diff --git a/internal/suites/example/kube/authelia/deployment.yml b/internal/suites/example/kube/authelia/deployment.yml
index 6de87f878..6a4156ecf 100644
--- a/internal/suites/example/kube/authelia/deployment.yml
+++ b/internal/suites/example/kube/authelia/deployment.yml
@@ -40,6 +40,8 @@ spec:
value: /app/secrets/session
- name: AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE
value: /app/secrets/sql_password
+ - name: AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE
+ value: /app/secrets/encryption_key
- name: ENVIRONMENT
value: dev
volumes:
@@ -69,4 +71,6 @@ spec:
path: sql_password
- key: ldap_password
path: ldap_password
+ - key: encryption_key
+ path: encryption_key
...
diff --git a/internal/suites/example/kube/authelia/secret.yml b/internal/suites/example/kube/authelia/secret.yml
index 8524cf115..de5e501e5 100644
--- a/internal/suites/example/kube/authelia/secret.yml
+++ b/internal/suites/example/kube/authelia/secret.yml
@@ -12,4 +12,5 @@ data:
ldap_password: cGFzc3dvcmQ= # password
session: dW5zZWN1cmVfcGFzc3dvcmQ= # unsecure_password
sql_password: cGFzc3dvcmQ= # password
+ encryption_key: YV9ub3Rfc29fc2VjdXJlX2VuY3J5cHRpb25fa2V5
...
diff --git a/internal/suites/suite_cli.go b/internal/suites/suite_cli.go
index c02d81c0e..44210af6a 100644
--- a/internal/suites/suite_cli.go
+++ b/internal/suites/suite_cli.go
@@ -2,6 +2,7 @@ package suites
import (
"fmt"
+ "os"
"time"
)
@@ -35,6 +36,9 @@ func init() {
teardown := func(suitePath string) error {
err := dockerEnvironment.Down()
+ _ = os.Remove("/tmp/db.sqlite3")
+ _ = os.Remove("/tmp/db.sqlite")
+
return err
}
diff --git a/internal/suites/suite_cli_test.go b/internal/suites/suite_cli_test.go
index b0999b720..fae91cda7 100644
--- a/internal/suites/suite_cli_test.go
+++ b/internal/suites/suite_cli_test.go
@@ -1,11 +1,18 @@
package suites
import (
+ "context"
+ "fmt"
"os"
"regexp"
"testing"
+ "github.com/pquerna/otp"
+ "github.com/pquerna/otp/totp"
"github.com/stretchr/testify/suite"
+
+ "github.com/authelia/authelia/v4/internal/models"
+ "github.com/authelia/authelia/v4/internal/storage"
)
type CLISuite struct {
@@ -40,7 +47,7 @@ func (s *CLISuite) SetupTest() {
func (s *CLISuite) TestShouldPrintBuildInformation() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "build-info"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Last Tag: ")
s.Assert().Contains(output, "State: ")
s.Assert().Contains(output, "Branch: ")
@@ -55,13 +62,13 @@ func (s *CLISuite) TestShouldPrintBuildInformation() {
func (s *CLISuite) TestShouldPrintVersion() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "--version"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "authelia version")
}
func (s *CLISuite) TestShouldValidateConfig() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "/config/configuration.yml"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Configuration parsed successfully without errors")
}
@@ -73,33 +80,33 @@ func (s *CLISuite) TestShouldFailValidateConfig() {
func (s *CLISuite) TestShouldHashPasswordArgon2id() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "hash-password", "test", "-m", "32", "-s", "test1234"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Password hash: $argon2id$v=19$m=32768,t=1,p=8")
}
func (s *CLISuite) TestShouldHashPasswordSHA512() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "hash-password", "test", "-z"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Password hash: $6$rounds=50000")
}
func (s *CLISuite) TestShouldGenerateCertificateRSA() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateRSAWithIPAddress() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "127.0.0.1", "--dir", "/tmp/"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateRSAWithStartDate() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--start-date", "'Jan 1 15:04:05 2011'"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
@@ -112,14 +119,14 @@ func (s *CLISuite) TestShouldFailGenerateCertificateRSAWithStartDate() {
func (s *CLISuite) TestShouldGenerateCertificateCA() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ca"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateEd25519() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ed25519"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
@@ -132,32 +139,238 @@ func (s *CLISuite) TestShouldFailGenerateCertificateECDSA() {
func (s *CLISuite) TestShouldGenerateCertificateECDSAP224() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P224"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP256() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P256"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP384() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P384"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP521() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P521"})
- s.Assert().Nil(err)
+ s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
+func (s *CLISuite) TestStorageShouldShowErrWithoutConfig() {
+ output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info"})
+ s.Assert().EqualError(err, "exit status 1")
+
+ s.Assert().Contains(output, "Error: A storage configuration must be provided. It could be 'local', 'mysql' or 'postgres', the configuration option storage.encryption_key must be provided\n")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "history"})
+ s.Assert().EqualError(err, "exit status 1")
+
+ s.Assert().Contains(output, "Error: A storage configuration must be provided. It could be 'local', 'mysql' or 'postgres', the configuration option storage.encryption_key must be provided\n")
+}
+
+func (s *CLISuite) TestStorage00ShouldShowCorrectPreInitInformation() {
+ output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
+ s.Assert().NoError(err)
+
+ pattern := regexp.MustCompile(`^Schema Version: N/A\nSchema Upgrade Available: yes - version \d+\nSchema Tables: N/A\nSchema Encryption Key: unsupported \(schema version\)`)
+
+ s.Assert().Regexp(pattern, output)
+
+ patternOutdated := regexp.MustCompile(`Error: schema is version \d+ which is outdated please migrate to version \d+ in order to use this command or use an older binary`)
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "export", "totp-configurations", "--config", "/config/configuration.storage.yml"})
+ s.Assert().EqualError(err, "exit status 1")
+ s.Assert().Regexp(patternOutdated, output)
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--config", "/config/configuration.storage.yml"})
+ s.Assert().EqualError(err, "exit status 1")
+ s.Assert().Regexp(patternOutdated, output)
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config", "/config/configuration.storage.yml"})
+ s.Assert().NoError(err)
+ s.Assert().Contains(output, "Could not check encryption key for validity. The schema version doesn't support encryption.")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target", "0", "--destroy-data", "--config", "/config/configuration.storage.yml"})
+ s.Assert().EqualError(err, "exit status 1")
+ s.Assert().Contains(output, "Error: schema migration target version 0 is the same current version 0")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "up", "--target", "2147483640", "--config", "/config/configuration.storage.yml"})
+ s.Assert().EqualError(err, "exit status 1")
+ s.Assert().Contains(output, "Error: schema up migration target version 2147483640 is greater then the latest version ")
+ s.Assert().Contains(output, " which indicates it doesn't exist")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "history"})
+ s.Assert().NoError(err)
+
+ s.Assert().Contains(output, "No migration history is available for schemas that not version 1 or above.\n")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-up"})
+ s.Assert().NoError(err)
+
+ s.Assert().Contains(output, "Storage Schema Migration List (Up)\n\nVersion\t\tDescription\n1\t\tInitial Schema\n")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-down"})
+ s.Assert().NoError(err)
+
+ s.Assert().Contains(output, "Storage Schema Migration List (Down)\n\nNo Migrations Available\n")
+}
+
+func (s *CLISuite) TestStorage01ShouldMigrateUp() {
+ output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "up"})
+ s.Require().NoError(err)
+
+ pattern0 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is being attempted"`)
+ pattern1 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is complete"`)
+
+ s.Regexp(pattern0, output)
+ s.Regexp(pattern1, output)
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "up"})
+ s.Assert().EqualError(err, "exit status 1")
+
+ s.Assert().Contains(output, "Error: schema already up to date\n")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "history"})
+ s.Assert().NoError(err)
+
+ s.Assert().Contains(output, "Migration History:\n\nID\tDate\t\t\t\tBefore\tAfter\tAuthelia Version\n")
+ s.Assert().Contains(output, "0\t1")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-up"})
+ s.Assert().NoError(err)
+
+ s.Assert().Contains(output, "Storage Schema Migration List (Up)\n\nNo Migrations Available")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-down"})
+ s.Assert().NoError(err)
+
+ s.Assert().Contains(output, "Storage Schema Migration List (Down)\n\nVersion\t\tDescription\n")
+ s.Assert().Contains(output, "1\t\tInitial Schema")
+}
+
+func (s *CLISuite) TestStorage02ShouldShowSchemaInfo() {
+ output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
+ s.Assert().NoError(err)
+
+ pattern := regexp.MustCompile(`^Schema Version: \d+\nSchema Upgrade Available: no\nSchema Tables: authentication_logs, identity_verification_tokens, totp_configurations, u2f_devices, user_preferences, migrations, encryption\nSchema Encryption Key: valid`)
+
+ s.Assert().Regexp(pattern, output)
+}
+
+func (s *CLISuite) TestStorage03ShouldExportTOTP() {
+ provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_cli_encryption_key_which_isnt_secure")
+
+ s.Require().NoError(provider.StartupCheck())
+
+ ctx := context.Background()
+
+ var (
+ err error
+ key *otp.Key
+ config models.TOTPConfiguration
+ )
+
+ var (
+ expectedLines = make([]string, 0, 3)
+ expectedLinesCSV = make([]string, 0, 4)
+ output string
+ )
+
+ expectedLinesCSV = append(expectedLinesCSV, "issuer,username,algorithm,digits,period,secret")
+
+ for _, name := range []string{"john", "mary", "fred"} {
+ key, err = totp.Generate(totp.GenerateOpts{
+ Issuer: "Authelia",
+ AccountName: name,
+ Period: uint(30),
+ SecretSize: 32,
+ Digits: otp.Digits(6),
+ Algorithm: otp.AlgorithmSHA1,
+ })
+ s.Require().NoError(err)
+
+ config = models.TOTPConfiguration{
+ Username: name,
+ Algorithm: "SHA1",
+ Digits: 6,
+ Secret: []byte(key.Secret()),
+ Period: key.Period(),
+ }
+
+ expectedLinesCSV = append(expectedLinesCSV, fmt.Sprintf("%s,%s,%s,%d,%d,%s", "Authelia", config.Username, config.Algorithm, config.Digits, config.Period, string(config.Secret)))
+ expectedLines = append(expectedLines, fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=%s&digits=%d&period=%d", "Authelia", config.Username, string(config.Secret), "Authelia", config.Algorithm, config.Digits, config.Period))
+
+ s.Require().NoError(provider.SaveTOTPConfiguration(ctx, config))
+ }
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "export", "totp-configurations", "--format", "uri", "--config", "/config/configuration.storage.yml"})
+ s.Assert().NoError(err)
+
+ for _, expectedLine := range expectedLines {
+ s.Assert().Contains(output, expectedLine)
+ }
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "export", "totp-configurations", "--format", "csv", "--config", "/config/configuration.storage.yml"})
+ s.Assert().NoError(err)
+
+ for _, expectedLine := range expectedLinesCSV {
+ s.Assert().Contains(output, expectedLine)
+ }
+}
+
+func (s *CLISuite) TestStorage04ShouldChangeEncryptionKey() {
+ output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--new-encryption-key", "apple-apple-apple-apple", "--config", "/config/configuration.storage.yml"})
+ s.Assert().NoError(err)
+
+ s.Assert().Contains(output, "Completed the encryption key change. Please adjust your configuration to use the new key.\n")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
+ s.Assert().NoError(err)
+
+ pattern := regexp.MustCompile(`Schema Version: \d+\nSchema Upgrade Available: no\nSchema Tables: authentication_logs, identity_verification_tokens, totp_configurations, u2f_devices, user_preferences, migrations, encryption\nSchema Encryption Key: invalid`)
+ s.Assert().Regexp(pattern, output)
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config", "/config/configuration.storage.yml"})
+ s.Assert().NoError(err)
+
+ s.Assert().Contains(output, "Encryption key validation: failed.\n\nError: the encryption key is not valid against the schema check value.\n")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--config", "/config/configuration.storage.yml"})
+ s.Assert().NoError(err)
+
+ s.Assert().Contains(output, "Encryption key validation: failed.\n\nError: the encryption key is not valid against the schema check value, 3 of 3 total TOTP secrets were invalid.\n")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--encryption-key", "apple-apple-apple-apple", "--config", "/config/configuration.storage.yml"})
+ s.Assert().NoError(err)
+
+ s.Assert().Contains(output, "Encryption key validation: success.\n")
+
+ output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--encryption-key", "apple-apple-apple-apple", "--config", "/config/configuration.storage.yml"})
+ s.Assert().NoError(err)
+
+ s.Assert().Contains(output, "Encryption key validation: success.\n")
+}
+
+func (s *CLISuite) TestStorage05ShouldMigrateDown() {
+ output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target", "0", "--destroy-data", "--config", "/config/configuration.storage.yml"})
+ s.Assert().NoError(err)
+
+ pattern0 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is being attempted"`)
+ pattern1 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is complete"`)
+
+ s.Regexp(pattern0, output)
+ s.Regexp(pattern1, output)
+}
+
func TestCLISuite(t *testing.T) {
if testing.Short() {
t.Skip("skipping suite test in short mode")
diff --git a/internal/suites/suite_standalone_test.go b/internal/suites/suite_standalone_test.go
index f02402027..75d3ab351 100644
--- a/internal/suites/suite_standalone_test.go
+++ b/internal/suites/suite_standalone_test.go
@@ -121,7 +121,7 @@ func (s *StandaloneWebDriverSuite) TestShouldCheckUserIsAskedToRegisterDevice()
password := "password"
// Clean up any TOTP secret already in DB.
- provider := storage.NewSQLiteProvider("/tmp/db.sqlite3")
+ provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key")
require.NoError(s.T(), provider.StartupCheck())
require.NoError(s.T(), provider.DeleteTOTPConfiguration(ctx, username))