sftpgo/internal/dataprovider/configs.go
Nicola Murino 784b7585c1
remove end year from Copyright notice in files
so we don't have to update all the files every year

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-01-01 11:31:45 +01:00

502 lines
14 KiB
Go

// Copyright (C) 2019 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package dataprovider
import (
"encoding/json"
"fmt"
"strings"
"golang.org/x/crypto/ssh"
"github.com/drakkan/sftpgo/v2/internal/kms"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/util"
)
// Supported values for host keys, KEXs, ciphers, MACs
var (
supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA}
supportedPublicKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.KeyAlgoDSA}
supportedKexAlgos = []string{
"diffie-hellman-group16-sha512", "diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1",
"diffie-hellman-group-exchange-sha256", "diffie-hellman-group-exchange-sha1",
}
supportedCiphers = []string{
"aes128-cbc", "aes192-cbc", "aes256-cbc",
"3des-cbc",
}
supportedMACs = []string{
"hmac-sha2-512-etm@openssh.com", "hmac-sha2-512",
"hmac-sha1", "hmac-sha1-96",
}
)
// SFTPDConfigs defines configurations for SFTPD
type SFTPDConfigs struct {
HostKeyAlgos []string `json:"host_key_algos,omitempty"`
PublicKeyAlgos []string `json:"public_key_algos,omitempty"`
Moduli []string `json:"moduli,omitempty"`
KexAlgorithms []string `json:"kex_algorithms,omitempty"`
Ciphers []string `json:"ciphers,omitempty"`
MACs []string `json:"macs,omitempty"`
}
func (c *SFTPDConfigs) isEmpty() bool {
if len(c.HostKeyAlgos) > 0 {
return false
}
if len(c.PublicKeyAlgos) > 0 {
return false
}
if len(c.Moduli) > 0 {
return false
}
if len(c.KexAlgorithms) > 0 {
return false
}
if len(c.Ciphers) > 0 {
return false
}
if len(c.MACs) > 0 {
return false
}
return true
}
// GetSupportedHostKeyAlgos returns the supported legacy host key algos
func (*SFTPDConfigs) GetSupportedHostKeyAlgos() []string {
return supportedHostKeyAlgos
}
// GetSupportedPublicKeyAlgos returns the supported legacy public key algos
func (*SFTPDConfigs) GetSupportedPublicKeyAlgos() []string {
return supportedPublicKeyAlgos
}
// GetSupportedKEXAlgos returns the supported KEX algos
func (*SFTPDConfigs) GetSupportedKEXAlgos() []string {
return supportedKexAlgos
}
// GetSupportedCiphers returns the supported ciphers
func (*SFTPDConfigs) GetSupportedCiphers() []string {
return supportedCiphers
}
// GetSupportedMACs returns the supported MACs algos
func (*SFTPDConfigs) GetSupportedMACs() []string {
return supportedMACs
}
// GetModuliAsString returns moduli files as comma separated string
func (c *SFTPDConfigs) GetModuliAsString() string {
return strings.Join(c.Moduli, ",")
}
func (c *SFTPDConfigs) validate() error {
var hostKeyAlgos []string
for _, algo := range c.HostKeyAlgos {
if algo == ssh.CertAlgoRSAv01 {
continue
}
if !util.Contains(supportedHostKeyAlgos, algo) {
return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo))
}
hostKeyAlgos = append(hostKeyAlgos, algo)
}
c.HostKeyAlgos = hostKeyAlgos
var kexAlgos []string
for _, algo := range c.KexAlgorithms {
if algo == "diffie-hellman-group18-sha512" {
continue
}
if !util.Contains(supportedKexAlgos, algo) {
return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
}
kexAlgos = append(kexAlgos, algo)
}
c.KexAlgorithms = kexAlgos
for _, cipher := range c.Ciphers {
if !util.Contains(supportedCiphers, cipher) {
return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher))
}
}
for _, mac := range c.MACs {
if !util.Contains(supportedMACs, mac) {
return util.NewValidationError(fmt.Sprintf("unsupported MAC algorithm %q", mac))
}
}
for _, algo := range c.PublicKeyAlgos {
if !util.Contains(supportedPublicKeyAlgos, algo) {
return util.NewValidationError(fmt.Sprintf("unsupported public key algorithm %q", algo))
}
}
return nil
}
func (c *SFTPDConfigs) getACopy() *SFTPDConfigs {
hostKeys := make([]string, len(c.HostKeyAlgos))
copy(hostKeys, c.HostKeyAlgos)
publicKeys := make([]string, len(c.PublicKeyAlgos))
copy(publicKeys, c.PublicKeyAlgos)
moduli := make([]string, len(c.Moduli))
copy(moduli, c.Moduli)
kexs := make([]string, len(c.KexAlgorithms))
copy(kexs, c.KexAlgorithms)
ciphers := make([]string, len(c.Ciphers))
copy(ciphers, c.Ciphers)
macs := make([]string, len(c.MACs))
copy(macs, c.MACs)
return &SFTPDConfigs{
HostKeyAlgos: hostKeys,
PublicKeyAlgos: publicKeys,
Moduli: moduli,
KexAlgorithms: kexs,
Ciphers: ciphers,
MACs: macs,
}
}
func validateSMTPSecret(secret *kms.Secret, name string) error {
if secret.IsRedacted() {
return util.NewValidationError(fmt.Sprintf("cannot save a redacted smtp %s", name))
}
if secret.IsEncrypted() && !secret.IsValid() {
return util.NewValidationError(fmt.Sprintf("invalid encrypted smtp %s", name))
}
if !secret.IsEmpty() && !secret.IsValidInput() {
return util.NewValidationError(fmt.Sprintf("invalid smtp %s", name))
}
if secret.IsPlain() {
secret.SetAdditionalData("smtp")
if err := secret.Encrypt(); err != nil {
return util.NewValidationError(fmt.Sprintf("could not encrypt smtp %s: %v", name, err))
}
}
return nil
}
// SMTPOAuth2 defines the SMTP related OAuth2 configurations
type SMTPOAuth2 struct {
Provider int `json:"provider,omitempty"`
Tenant string `json:"tenant,omitempty"`
ClientID string `json:"client_id,omitempty"`
ClientSecret *kms.Secret `json:"client_secret,omitempty"`
RefreshToken *kms.Secret `json:"refresh_token,omitempty"`
}
func (c *SMTPOAuth2) validate() error {
if c.Provider < 0 || c.Provider > 1 {
return util.NewValidationError("smtp oauth2: unsupported provider")
}
if c.ClientID == "" {
return util.NewValidationError("smtp oauth2: client id is required")
}
if c.ClientSecret == nil {
return util.NewValidationError("smtp oauth2: client secret is required")
}
if c.RefreshToken == nil {
return util.NewValidationError("smtp oauth2: refresh token is required")
}
if err := validateSMTPSecret(c.ClientSecret, "oauth2 client secret"); err != nil {
return err
}
return validateSMTPSecret(c.RefreshToken, "oauth2 refresh token")
}
func (c *SMTPOAuth2) getACopy() SMTPOAuth2 {
var clientSecret, refreshToken *kms.Secret
if c.ClientSecret != nil {
clientSecret = c.ClientSecret.Clone()
}
if c.RefreshToken != nil {
refreshToken = c.RefreshToken.Clone()
}
return SMTPOAuth2{
Provider: c.Provider,
Tenant: c.Tenant,
ClientID: c.ClientID,
ClientSecret: clientSecret,
RefreshToken: refreshToken,
}
}
// SMTPConfigs defines configuration for SMTP
type SMTPConfigs struct {
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
From string `json:"from,omitempty"`
User string `json:"user,omitempty"`
Password *kms.Secret `json:"password,omitempty"`
AuthType int `json:"auth_type,omitempty"`
Encryption int `json:"encryption,omitempty"`
Domain string `json:"domain,omitempty"`
Debug int `json:"debug,omitempty"`
OAuth2 SMTPOAuth2 `json:"oauth2"`
}
// IsEmpty returns true if the configuration is empty
func (c *SMTPConfigs) IsEmpty() bool {
return c.Host == ""
}
func (c *SMTPConfigs) validate() error {
if c.IsEmpty() {
return nil
}
if c.Port <= 0 || c.Port > 65535 {
return util.NewValidationError(fmt.Sprintf("smtp: invalid port %d", c.Port))
}
if c.Password != nil && c.AuthType != 3 {
if err := validateSMTPSecret(c.Password, "password"); err != nil {
return err
}
}
if c.User == "" && c.From == "" {
return util.NewValidationError("smtp: from address and user cannot both be empty")
}
if c.AuthType < 0 || c.AuthType > 3 {
return util.NewValidationError(fmt.Sprintf("smtp: invalid auth type %d", c.AuthType))
}
if c.Encryption < 0 || c.Encryption > 2 {
return util.NewValidationError(fmt.Sprintf("smtp: invalid encryption %d", c.Encryption))
}
if c.AuthType == 3 {
c.Password = kms.NewEmptySecret()
return c.OAuth2.validate()
}
c.OAuth2 = SMTPOAuth2{}
return nil
}
// TryDecrypt tries to decrypt the encrypted secrets
func (c *SMTPConfigs) TryDecrypt() error {
if c.Password == nil {
c.Password = kms.NewEmptySecret()
}
if c.OAuth2.ClientSecret == nil {
c.OAuth2.ClientSecret = kms.NewEmptySecret()
}
if c.OAuth2.RefreshToken == nil {
c.OAuth2.RefreshToken = kms.NewEmptySecret()
}
if err := c.Password.TryDecrypt(); err != nil {
return fmt.Errorf("unable to decrypt smtp password: %w", err)
}
if err := c.OAuth2.ClientSecret.TryDecrypt(); err != nil {
return fmt.Errorf("unable to decrypt smtp oauth2 client secret: %w", err)
}
if err := c.OAuth2.RefreshToken.TryDecrypt(); err != nil {
return fmt.Errorf("unable to decrypt smtp oauth2 refresh token: %w", err)
}
return nil
}
func (c *SMTPConfigs) getACopy() *SMTPConfigs {
var password *kms.Secret
if c.Password != nil {
password = c.Password.Clone()
}
return &SMTPConfigs{
Host: c.Host,
Port: c.Port,
From: c.From,
User: c.User,
Password: password,
AuthType: c.AuthType,
Encryption: c.Encryption,
Domain: c.Domain,
Debug: c.Debug,
OAuth2: c.OAuth2.getACopy(),
}
}
// ACMEHTTP01Challenge defines the configuration for HTTP-01 challenge type
type ACMEHTTP01Challenge struct {
Port int `json:"port"`
}
// ACMEConfigs defines ACME related configuration
type ACMEConfigs struct {
Domain string `json:"domain"`
Email string `json:"email"`
HTTP01Challenge ACMEHTTP01Challenge `json:"http01_challenge"`
// apply the certificate for the specified protocols:
//
// 1 means HTTP
// 2 means FTP
// 4 means WebDAV
//
// Protocols can be combined
Protocols int `json:"protocols"`
}
func (c *ACMEConfigs) isEmpty() bool {
return c.Domain == ""
}
func (c *ACMEConfigs) validate() error {
if c.Domain == "" {
return nil
}
if c.Email == "" && !util.IsEmailValid(c.Email) {
return util.NewValidationError(fmt.Sprintf("acme: invalid email %q", c.Email))
}
if c.HTTP01Challenge.Port <= 0 || c.HTTP01Challenge.Port > 65535 {
return util.NewValidationError(fmt.Sprintf("acme: invalid HTTP-01 challenge port %d", c.HTTP01Challenge.Port))
}
return nil
}
// HasProtocol returns true if the ACME certificate must be used for the specified protocol
func (c *ACMEConfigs) HasProtocol(protocol string) bool {
switch protocol {
case protocolHTTP:
return c.Protocols&1 != 0
case protocolFTP:
return c.Protocols&2 != 0
case protocolWebDAV:
return c.Protocols&4 != 0
default:
return false
}
}
func (c *ACMEConfigs) getACopy() *ACMEConfigs {
return &ACMEConfigs{
Email: c.Email,
Domain: c.Domain,
HTTP01Challenge: ACMEHTTP01Challenge{Port: c.HTTP01Challenge.Port},
Protocols: c.Protocols,
}
}
// Configs allows to set configuration keys disabled by default without
// modifying the config file or setting env vars
type Configs struct {
SFTPD *SFTPDConfigs `json:"sftpd,omitempty"`
SMTP *SMTPConfigs `json:"smtp,omitempty"`
ACME *ACMEConfigs `json:"acme,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
}
func (c *Configs) validate() error {
if c.SFTPD != nil {
if err := c.SFTPD.validate(); err != nil {
return err
}
}
if c.SMTP != nil {
if err := c.SMTP.validate(); err != nil {
return err
}
}
if c.ACME != nil {
if err := c.ACME.validate(); err != nil {
return err
}
}
return nil
}
// PrepareForRendering prepares configs for rendering.
// It hides confidential data and set to nil the empty structs/secrets
// so they are not serialized
func (c *Configs) PrepareForRendering() {
if c.SFTPD != nil && c.SFTPD.isEmpty() {
c.SFTPD = nil
}
if c.SMTP != nil && c.SMTP.IsEmpty() {
c.SMTP = nil
}
if c.ACME != nil && c.ACME.isEmpty() {
c.ACME = nil
}
if c.SMTP != nil {
if c.SMTP.Password != nil {
c.SMTP.Password.Hide()
if c.SMTP.Password.IsEmpty() {
c.SMTP.Password = nil
}
}
if c.SMTP.OAuth2.ClientSecret != nil {
c.SMTP.OAuth2.ClientSecret.Hide()
if c.SMTP.OAuth2.ClientSecret.IsEmpty() {
c.SMTP.OAuth2.ClientSecret = nil
}
}
if c.SMTP.OAuth2.RefreshToken != nil {
c.SMTP.OAuth2.RefreshToken.Hide()
if c.SMTP.OAuth2.RefreshToken.IsEmpty() {
c.SMTP.OAuth2.RefreshToken = nil
}
}
}
}
// SetNilsToEmpty sets nil fields to empty
func (c *Configs) SetNilsToEmpty() {
if c.SFTPD == nil {
c.SFTPD = &SFTPDConfigs{}
}
if c.SMTP == nil {
c.SMTP = &SMTPConfigs{}
}
if c.SMTP.Password == nil {
c.SMTP.Password = kms.NewEmptySecret()
}
if c.SMTP.OAuth2.ClientSecret == nil {
c.SMTP.OAuth2.ClientSecret = kms.NewEmptySecret()
}
if c.SMTP.OAuth2.RefreshToken == nil {
c.SMTP.OAuth2.RefreshToken = kms.NewEmptySecret()
}
if c.ACME == nil {
c.ACME = &ACMEConfigs{}
}
}
// RenderAsJSON implements the renderer interface used within plugins
func (c *Configs) RenderAsJSON(reload bool) ([]byte, error) {
if reload {
config, err := provider.getConfigs()
if err != nil {
providerLog(logger.LevelError, "unable to reload config overrides before rendering as json: %v", err)
return nil, err
}
config.PrepareForRendering()
return json.Marshal(config)
}
c.PrepareForRendering()
return json.Marshal(c)
}
func (c *Configs) getACopy() Configs {
var result Configs
if c.SFTPD != nil {
result.SFTPD = c.SFTPD.getACopy()
}
if c.SMTP != nil {
result.SMTP = c.SMTP.getACopy()
}
if c.ACME != nil {
result.ACME = c.ACME.getACopy()
}
result.UpdatedAt = c.UpdatedAt
return result
}