| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- package vfs
- import (
- "crypto/aes"
- "crypto/cipher"
- "crypto/rand"
- "crypto/sha256"
- "encoding/hex"
- "errors"
- "io"
- "github.com/drakkan/sftpgo/utils"
- )
- // 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"
- // SecretStatusRedacted means the secret is redacted
- SecretStatusRedacted SecretStatus = "Redacted"
- )
- var (
- errWrongSecretStatus = errors.New("wrong secret status")
- errMalformedCiphertext = errors.New("malformed ciphertext")
- errInvalidSecret = errors.New("invalid secret")
- validSecretStatuses = []string{SecretStatusPlain, SecretStatusAES256GCM, SecretStatusRedacted}
- )
- // Secret defines the struct used to store confidential data
- type Secret struct {
- Status SecretStatus `json:"status,omitempty"`
- Payload string `json:"payload,omitempty"`
- Key string `json:"key,omitempty"`
- AdditionalData string `json:"additional_data,omitempty"`
- }
- // GetSecretFromCompatString returns a secret from the previous format
- func GetSecretFromCompatString(secret string) (Secret, error) {
- s := Secret{}
- plain, err := utils.DecryptData(secret)
- if err != nil {
- return s, errMalformedCiphertext
- }
- s.Status = SecretStatusPlain
- s.Payload = plain
- return s, nil
- }
- // 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 {
- return s.Status == SecretStatusAES256GCM
- }
- // IsPlain returns true if the secret is in plain text
- func (s *Secret) IsPlain() bool {
- return s.Status == SecretStatusPlain
- }
- // IsRedacted returns true if the secret is redacted
- func (s *Secret) IsRedacted() bool {
- return s.Status == SecretStatusRedacted
- }
- // IsEmpty returns true if all fields are empty
- func (s *Secret) IsEmpty() bool {
- if s.Status != "" {
- return false
- }
- if s.Payload != "" {
- return false
- }
- if s.Key != "" {
- return false
- }
- if s.AdditionalData != "" {
- return false
- }
- return true
- }
- // IsValid returns true if the secret is not empty and valid
- func (s *Secret) IsValid() bool {
- if !s.IsValidInput() {
- return false
- }
- if s.Status == SecretStatusAES256GCM {
- if len(s.Key) != 64 {
- return false
- }
- }
- return true
- }
- // IsValidInput returns true if the secret is a valid user input
- func (s *Secret) IsValidInput() bool {
- if !utils.IsStringInSlice(s.Status, validSecretStatuses) {
- return false
- }
- if s.Payload == "" {
- return false
- }
- return true
- }
- // Hide hides info to decrypt data
- func (s *Secret) Hide() {
- s.Key = ""
- s.AdditionalData = ""
- }
- // deriveKey is a weak method of deriving a key but it is still better than using the key as it is.
- // We should use a KMS in future
- func (s *Secret) deriveKey(key []byte) []byte {
- var combined []byte
- combined = append(combined, key...)
- if s.AdditionalData != "" {
- combined = append(combined, []byte(s.AdditionalData)...)
- }
- combined = append(combined, key...)
- hash := sha256.Sum256(combined)
- return hash[:]
- }
- // Encrypt encrypts a plain text Secret object
- func (s *Secret) Encrypt() error {
- if s.Payload == "" {
- return errInvalidSecret
- }
- switch s.Status {
- case SecretStatusPlain:
- key := make([]byte, 32)
- if _, err := io.ReadFull(rand.Reader, key); err != nil {
- return err
- }
- block, err := aes.NewCipher(s.deriveKey(key))
- if err != nil {
- return err
- }
- gcm, err := cipher.NewGCM(block)
- if err != nil {
- return err
- }
- nonce := make([]byte, gcm.NonceSize())
- if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
- return err
- }
- var aad []byte
- if s.AdditionalData != "" {
- aad = []byte(s.AdditionalData)
- }
- ciphertext := gcm.Seal(nonce, nonce, []byte(s.Payload), aad)
- s.Key = hex.EncodeToString(key)
- s.Payload = hex.EncodeToString(ciphertext)
- s.Status = SecretStatusAES256GCM
- return nil
- default:
- return errWrongSecretStatus
- }
- }
- // Decrypt decrypts a Secret object
- func (s *Secret) Decrypt() error {
- switch s.Status {
- case SecretStatusAES256GCM:
- encrypted, err := hex.DecodeString(s.Payload)
- if err != nil {
- return err
- }
- key, err := hex.DecodeString(s.Key)
- if err != nil {
- return err
- }
- block, err := aes.NewCipher(s.deriveKey(key))
- if err != nil {
- return err
- }
- gcm, err := cipher.NewGCM(block)
- if err != nil {
- return err
- }
- nonceSize := gcm.NonceSize()
- if len(encrypted) < nonceSize {
- return errMalformedCiphertext
- }
- nonce, ciphertext := encrypted[:nonceSize], encrypted[nonceSize:]
- var aad []byte
- if s.AdditionalData != "" {
- aad = []byte(s.AdditionalData)
- }
- plaintext, err := gcm.Open(nil, nonce, ciphertext, aad)
- if err != nil {
- return err
- }
- s.Status = SecretStatusPlain
- s.Payload = string(plaintext)
- s.Key = ""
- s.AdditionalData = ""
- return nil
- default:
- return errWrongSecretStatus
- }
- }
|