authelia/internal/storage/sql_provider_encryption.go
James Elliott 8f05846e21
feat: webauthn (#2707)
This implements Webauthn. Old devices can be used to authenticate via the appid compatibility layer which should be automatic. New devices will be registered via Webauthn, and devices which do not support FIDO2 will no longer be able to be registered. At this time it does not fully support multiple devices (backend does, frontend doesn't allow registration of additional devices). Does not support passwordless.
2022-03-03 22:20:43 +11:00

300 lines
7.0 KiB
Go

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))
if err = p.schemaEncryptionChangeKeyTOTP(ctx, tx, key); err != nil {
return err
}
if err = p.schemaEncryptionChangeKeyWebauthn(ctx, tx, key); err != nil {
return err
}
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()
}
func (p *SQLProvider) schemaEncryptionChangeKeyTOTP(ctx context.Context, tx *sqlx.Tx, key [32]byte) (err error) {
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
}
}
return nil
}
func (p *SQLProvider) schemaEncryptionChangeKeyWebauthn(ctx context.Context, tx *sqlx.Tx, key [32]byte) (err error) {
var devices []models.WebauthnDevice
for page := 0; true; page++ {
if devices, err = p.LoadWebauthnDevices(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 _, device := range devices {
if device.PublicKey, err = utils.Encrypt(device.PublicKey, &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.updateWebauthnDevicePublicKey(ctx, device); 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(devices) != 10 {
break
}
}
return nil
}
// 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 {
if err = p.schemaEncryptionCheckTOTP(ctx); err != nil {
errs = append(errs, err)
}
if err = p.schemaEncryptionCheckU2F(ctx); err != nil {
errs = append(errs, err)
}
}
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) schemaEncryptionCheckTOTP(ctx context.Context) (err error) {
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 {
return fmt.Errorf("%d of %d total TOTP secrets were invalid", invalid, total)
}
return nil
}
func (p *SQLProvider) schemaEncryptionCheckU2F(ctx context.Context) (err error) {
var (
device models.U2FDevice
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.sqlSelectWebauthnDevices, pageSize, pageSize*page); err != nil {
_ = rows.Close()
return fmt.Errorf("error selecting U2F devices: %w", err)
}
row = 0
for rows.Next() {
total++
row++
if err = rows.StructScan(&device); err != nil {
_ = rows.Close()
return fmt.Errorf("error scanning U2F device to struct: %w", err)
}
if _, err = p.decrypt(device.PublicKey); err != nil {
invalid++
}
}
_ = rows.Close()
if row < pageSize {
break
}
}
if invalid != 0 {
return fmt.Errorf("%d of %d total U2F devices were invalid", invalid, total)
}
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, err := uuid.NewUUID()
if err != nil {
return err
}
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
}