| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- // Package kms provides Key Management Services support
- package kms
- import (
- "encoding/json"
- "errors"
- "os"
- "strings"
- "sync"
- "github.com/drakkan/sftpgo/v2/logger"
- "github.com/drakkan/sftpgo/v2/util"
- )
- // SecretProvider defines the interface for a KMS secrets provider
- type SecretProvider interface {
- Name() string
- Encrypt() error
- Decrypt() error
- IsEncrypted() bool
- GetStatus() SecretStatus
- GetPayload() string
- GetKey() string
- GetAdditionalData() string
- GetMode() int
- SetKey(string)
- SetAdditionalData(string)
- SetStatus(SecretStatus)
- Clone() SecretProvider
- }
- const (
- logSender = "kms"
- )
- // SecretStatus defines the statuses of a Secret object
- type SecretStatus = string
- const (
- // SecretStatusPlain means the secret is in plain text and must be encrypted
- SecretStatusPlain SecretStatus = "Plain"
- // SecretStatusAES256GCM means the secret is encrypted using AES-256-GCM
- SecretStatusAES256GCM SecretStatus = "AES-256-GCM"
- // SecretStatusSecretBox means the secret is encrypted using a locally provided symmetric key
- SecretStatusSecretBox SecretStatus = "Secretbox"
- // SecretStatusGCP means we use keys from Google Cloud Platform’s Key Management Service
- // (GCP KMS) to keep information secret
- SecretStatusGCP SecretStatus = "GCP"
- // SecretStatusAWS means we use customer master keys from Amazon Web Service’s
- // Key Management Service (AWS KMS) to keep information secret
- SecretStatusAWS SecretStatus = "AWS"
- // SecretStatusVaultTransit means we use the transit secrets engine in Vault
- // to keep information secret
- SecretStatusVaultTransit SecretStatus = "VaultTransit"
- // SecretStatusAzureKeyVault means we use Azure KeyVault to keep information secret
- SecretStatusAzureKeyVault SecretStatus = "AzureKeyVault"
- // SecretStatusRedacted means the secret is redacted
- SecretStatusRedacted SecretStatus = "Redacted"
- )
- // Scheme defines the supported URL scheme
- type Scheme = string
- // supported URL schemes
- const (
- SchemeLocal Scheme = "local"
- SchemeBuiltin Scheme = "builtin"
- SchemeAWS Scheme = "awskms"
- SchemeGCP Scheme = "gcpkms"
- SchemeVaultTransit Scheme = "hashivault"
- SchemeAzureKeyVault Scheme = "azurekeyvault"
- )
- // Configuration defines the KMS configuration
- type Configuration struct {
- Secrets Secrets `json:"secrets" mapstructure:"secrets"`
- }
- // Secrets define the KMS configuration for encryption/decryption
- type Secrets struct {
- URL string `json:"url" mapstructure:"url"`
- MasterKeyPath string `json:"master_key_path" mapstructure:"master_key_path"`
- masterKey string
- }
- type registeredSecretProvider struct {
- encryptedStatus SecretStatus
- newFn func(base BaseSecret, url, masterKey string) SecretProvider
- }
- var (
- // ErrWrongSecretStatus defines the error to return if the secret status is not appropriate
- // for the request operation
- ErrWrongSecretStatus = errors.New("wrong secret status")
- // ErrInvalidSecret defines the error to return if a secret is not valid
- ErrInvalidSecret = errors.New("invalid secret")
- errMalformedCiphertext = errors.New("malformed ciphertext")
- validSecretStatuses = []string{SecretStatusPlain, SecretStatusAES256GCM, SecretStatusSecretBox,
- SecretStatusVaultTransit, SecretStatusAWS, SecretStatusGCP, SecretStatusRedacted}
- config Configuration
- secretProviders = make(map[string]registeredSecretProvider)
- )
- // RegisterSecretProvider register a new secret provider
- func RegisterSecretProvider(scheme string, encryptedStatus SecretStatus, fn func(base BaseSecret, url, masterKey string) SecretProvider) {
- secretProviders[scheme] = registeredSecretProvider{
- encryptedStatus: encryptedStatus,
- newFn: fn,
- }
- }
- // NewSecret builds a new Secret using the provided arguments
- func NewSecret(status SecretStatus, payload, key, data string) *Secret {
- return config.newSecret(status, payload, key, data)
- }
- // NewEmptySecret returns an empty secret
- func NewEmptySecret() *Secret {
- return NewSecret("", "", "", "")
- }
- // NewPlainSecret stores the give payload in a plain text secret
- func NewPlainSecret(payload string) *Secret {
- return NewSecret(SecretStatusPlain, payload, "", "")
- }
- // GetSecretFromCompatString returns a secret from the previous format
- func GetSecretFromCompatString(secret string) (*Secret, error) {
- plain, err := util.DecryptData(secret)
- if err != nil {
- return &Secret{}, errMalformedCiphertext
- }
- return NewSecret(SecretStatusPlain, plain, "", ""), nil
- }
- // Initialize configures the KMS support
- func (c *Configuration) Initialize() error {
- if c.Secrets.MasterKeyPath != "" {
- mKey, err := os.ReadFile(c.Secrets.MasterKeyPath)
- if err != nil {
- return err
- }
- c.Secrets.masterKey = strings.TrimSpace(string(mKey))
- }
- config = *c
- if config.Secrets.URL == "" {
- config.Secrets.URL = SchemeLocal + "://"
- }
- for k, v := range secretProviders {
- logger.Debug(logSender, "", "secret provider registered for scheme: %#v, encrypted status: %#v",
- k, v.encryptedStatus)
- }
- return nil
- }
- func (c *Configuration) newSecret(status SecretStatus, payload, key, data string) *Secret {
- base := BaseSecret{
- Status: status,
- Key: key,
- Payload: payload,
- AdditionalData: data,
- }
- return &Secret{
- provider: c.getSecretProvider(base),
- }
- }
- func (c *Configuration) getSecretProvider(base BaseSecret) SecretProvider {
- for k, v := range secretProviders {
- if strings.HasPrefix(c.Secrets.URL, k) {
- return v.newFn(base, c.Secrets.URL, c.Secrets.masterKey)
- }
- }
- logger.Warn(logSender, "", "no secret provider registered for URL %v, fallback to local provider", c.Secrets.URL)
- return NewLocalSecret(base, c.Secrets.URL, c.Secrets.masterKey)
- }
- // Secret defines the struct used to store confidential data
- type Secret struct {
- sync.RWMutex
- provider SecretProvider
- }
- // MarshalJSON return the JSON encoding of the Secret object
- func (s *Secret) MarshalJSON() ([]byte, error) {
- s.RLock()
- defer s.RUnlock()
- return json.Marshal(&BaseSecret{
- Status: s.provider.GetStatus(),
- Payload: s.provider.GetPayload(),
- Key: s.provider.GetKey(),
- AdditionalData: s.provider.GetAdditionalData(),
- Mode: s.provider.GetMode(),
- })
- }
- // UnmarshalJSON parses the JSON-encoded data and stores the result
- // in the Secret object
- func (s *Secret) UnmarshalJSON(data []byte) error {
- s.Lock()
- defer s.Unlock()
- baseSecret := BaseSecret{}
- err := json.Unmarshal(data, &baseSecret)
- if err != nil {
- return err
- }
- if baseSecret.isEmpty() {
- s.provider = config.getSecretProvider(baseSecret)
- return nil
- }
- if baseSecret.Status == SecretStatusPlain || baseSecret.Status == SecretStatusRedacted {
- s.provider = config.getSecretProvider(baseSecret)
- return nil
- }
- for _, v := range secretProviders {
- if v.encryptedStatus == baseSecret.Status {
- s.provider = v.newFn(baseSecret, config.Secrets.URL, config.Secrets.masterKey)
- return nil
- }
- }
- logger.Debug(logSender, "", "no provider registered for status %#v", baseSecret.Status)
- return ErrInvalidSecret
- }
- // IsEqual returns true if all the secrets fields are equal
- func (s *Secret) IsEqual(other *Secret) bool {
- if s.GetStatus() != other.GetStatus() {
- return false
- }
- if s.GetPayload() != other.GetPayload() {
- return false
- }
- if s.GetKey() != other.GetKey() {
- return false
- }
- if s.GetAdditionalData() != other.GetAdditionalData() {
- return false
- }
- if s.GetMode() != other.GetMode() {
- return false
- }
- return true
- }
- // Clone returns a copy of the secret object
- func (s *Secret) Clone() *Secret {
- s.RLock()
- defer s.RUnlock()
- return &Secret{
- provider: s.provider.Clone(),
- }
- }
- // IsEncrypted returns true if the secret is encrypted
- // This isn't a pointer receiver because we don't want to pass
- // a pointer to html template
- func (s *Secret) IsEncrypted() bool {
- s.RLock()
- defer s.RUnlock()
- return s.provider.IsEncrypted()
- }
- // IsPlain returns true if the secret is in plain text
- func (s *Secret) IsPlain() bool {
- s.RLock()
- defer s.RUnlock()
- return s.provider.GetStatus() == SecretStatusPlain
- }
- // IsNotPlainAndNotEmpty returns true if the secret is not plain and not empty.
- // This is an utility method, we update the secret for an existing user
- // if it is empty or plain
- func (s *Secret) IsNotPlainAndNotEmpty() bool {
- s.RLock()
- defer s.RUnlock()
- return !s.IsPlain() && !s.IsEmpty()
- }
- // IsRedacted returns true if the secret is redacted
- func (s *Secret) IsRedacted() bool {
- s.RLock()
- defer s.RUnlock()
- return s.provider.GetStatus() == SecretStatusRedacted
- }
- // GetPayload returns the secret payload
- func (s *Secret) GetPayload() string {
- s.RLock()
- defer s.RUnlock()
- return s.provider.GetPayload()
- }
- // GetAdditionalData returns the secret additional data
- func (s *Secret) GetAdditionalData() string {
- s.RLock()
- defer s.RUnlock()
- return s.provider.GetAdditionalData()
- }
- // GetStatus returns the secret status
- func (s *Secret) GetStatus() SecretStatus {
- s.RLock()
- defer s.RUnlock()
- return s.provider.GetStatus()
- }
- // GetKey returns the secret key
- func (s *Secret) GetKey() string {
- s.RLock()
- defer s.RUnlock()
- return s.provider.GetKey()
- }
- // GetMode returns the secret mode
- func (s *Secret) GetMode() int {
- s.RLock()
- defer s.RUnlock()
- return s.provider.GetMode()
- }
- // SetAdditionalData sets the given additional data
- func (s *Secret) SetAdditionalData(value string) {
- s.Lock()
- defer s.Unlock()
- s.provider.SetAdditionalData(value)
- }
- // SetStatus sets the status for this secret
- func (s *Secret) SetStatus(value SecretStatus) {
- s.Lock()
- defer s.Unlock()
- s.provider.SetStatus(value)
- }
- // SetKey sets the key for this secret
- func (s *Secret) SetKey(value string) {
- s.Lock()
- defer s.Unlock()
- s.provider.SetKey(value)
- }
- // IsEmpty returns true if all fields are empty
- func (s *Secret) IsEmpty() bool {
- s.RLock()
- defer s.RUnlock()
- if s.provider.GetStatus() != "" {
- return false
- }
- if s.provider.GetPayload() != "" {
- return false
- }
- if s.provider.GetKey() != "" {
- return false
- }
- if s.provider.GetAdditionalData() != "" {
- return false
- }
- return true
- }
- // IsValid returns true if the secret is not empty and valid
- func (s *Secret) IsValid() bool {
- s.RLock()
- defer s.RUnlock()
- if !s.IsValidInput() {
- return false
- }
- switch s.provider.GetStatus() {
- case SecretStatusAES256GCM, SecretStatusSecretBox:
- if len(s.provider.GetKey()) != 64 {
- return false
- }
- case SecretStatusAWS, SecretStatusGCP, SecretStatusVaultTransit:
- key := s.provider.GetKey()
- if key != "" && len(key) != 64 {
- return false
- }
- }
- return true
- }
- // IsValidInput returns true if the secret is a valid user input
- func (s *Secret) IsValidInput() bool {
- s.RLock()
- defer s.RUnlock()
- if !util.IsStringInSlice(s.provider.GetStatus(), validSecretStatuses) {
- return false
- }
- if s.provider.GetPayload() == "" {
- return false
- }
- return true
- }
- // Hide hides info to decrypt data
- func (s *Secret) Hide() {
- s.Lock()
- defer s.Unlock()
- s.provider.SetKey("")
- s.provider.SetAdditionalData("")
- }
- // Encrypt encrypts a plain text Secret object
- func (s *Secret) Encrypt() error {
- s.Lock()
- defer s.Unlock()
- return s.provider.Encrypt()
- }
- // Decrypt decrypts a Secret object
- func (s *Secret) Decrypt() error {
- s.Lock()
- defer s.Unlock()
- return s.provider.Decrypt()
- }
- // TryDecrypt decrypts a Secret object if encrypted.
- // It returns a nil error if the object is not encrypted
- func (s *Secret) TryDecrypt() error {
- s.Lock()
- defer s.Unlock()
- if s.provider.IsEncrypted() {
- return s.provider.Decrypt()
- }
- return nil
- }
|