diff --git a/pkg/api/login.go b/pkg/api/login.go index 1090d668ebb..0a37772d52a 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -279,12 +279,12 @@ func tryGetEncryptedCookie(ctx *models.ReqContext, cookieName string) (string, b return "", false } - decryptedError, err := util.Decrypt(decoded, setting.SecretKey) + decryptedError, err := util.Decrypt(decoded) return string(decryptedError), err == nil } func (hs *HTTPServer) trySetEncryptedCookie(ctx *models.ReqContext, cookieName string, value string, maxAge int) error { - encryptedError, err := util.Encrypt([]byte(value), setting.SecretKey) + encryptedError, err := util.Encrypt([]byte(value)) if err != nil { return err } diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index a84668bfda8..c3b09d1b852 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -109,7 +109,7 @@ func TestLoginErrorCookieApiEndpoint(t *testing.T) { setting.OAuthAutoLogin = true oauthError := errors.New("User not a member of one of the required organizations") - encryptedError, _ := util.Encrypt([]byte(oauthError.Error()), setting.SecretKey) + encryptedError, _ := util.Encrypt([]byte(oauthError.Error())) expCookiePath := "/" if len(setting.AppSubUrl) > 0 { expCookiePath = setting.AppSubUrl diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/encrypt_datasource_passwords.go b/pkg/cmd/grafana-cli/commands/datamigrations/encrypt_datasource_passwords.go index 00e894f4c49..f22c55b51f3 100644 --- a/pkg/cmd/grafana-cli/commands/datamigrations/encrypt_datasource_passwords.go +++ b/pkg/cmd/grafana-cli/commands/datamigrations/encrypt_datasource_passwords.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -109,7 +108,7 @@ func updateRows(session *sqlstore.DBSession, rows []map[string][]byte, passwordF } func getUpdatedSecureJSONData(row map[string][]byte, passwordFieldName string) (map[string]interface{}, error) { - encryptedPassword, err := util.Encrypt(row[passwordFieldName], setting.SecretKey) + encryptedPassword, err := util.Encrypt(row[passwordFieldName]) if err != nil { return nil, err } diff --git a/pkg/components/securejsondata/securejsondata.go b/pkg/components/securejsondata/securejsondata.go index 304aa3c064f..6d253770886 100644 --- a/pkg/components/securejsondata/securejsondata.go +++ b/pkg/components/securejsondata/securejsondata.go @@ -2,7 +2,6 @@ package securejsondata import ( "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -14,7 +13,7 @@ type SecureJsonData map[string][]byte // is true if the key exists and false if not. func (s SecureJsonData) DecryptedValue(key string) (string, bool) { if value, ok := s[key]; ok { - decryptedData, err := util.Decrypt(value, setting.SecretKey) + decryptedData, err := util.Decrypt(value) if err != nil { log.Fatalf(4, err.Error()) } @@ -28,7 +27,7 @@ func (s SecureJsonData) DecryptedValue(key string) (string, bool) { func (s SecureJsonData) Decrypt() map[string]string { decrypted := make(map[string]string) for key, data := range s { - decryptedData, err := util.Decrypt(data, setting.SecretKey) + decryptedData, err := util.Decrypt(data) if err != nil { log.Fatalf(4, err.Error()) } @@ -42,7 +41,7 @@ func (s SecureJsonData) Decrypt() map[string]string { func GetEncryptedJsonData(sjd map[string]string) SecureJsonData { encrypted := make(SecureJsonData) for key, data := range sjd { - encryptedData, err := util.Encrypt([]byte(data), setting.SecretKey) + encryptedData, err := util.Encrypt([]byte(data)) if err != nil { log.Fatalf(4, err.Error()) } diff --git a/pkg/server/server.go b/pkg/server/server.go index feeaabc961b..e9a9af47d25 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -39,6 +39,7 @@ import ( _ "github.com/grafana/grafana/pkg/services/provisioning" _ "github.com/grafana/grafana/pkg/services/rendering" _ "github.com/grafana/grafana/pkg/services/search" + _ "github.com/grafana/grafana/pkg/services/secrets" _ "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" diff --git a/pkg/services/secrets/encryption.go b/pkg/services/secrets/encryption.go new file mode 100644 index 00000000000..fe73cb5edf2 --- /dev/null +++ b/pkg/services/secrets/encryption.go @@ -0,0 +1,80 @@ +package secrets + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "errors" + "io" + + "github.com/grafana/grafana/pkg/util" + "golang.org/x/crypto/pbkdf2" +) + +const saltLength = 8 + +// Decrypt decrypts a payload with a given secret. +func decrypt(payload, secret []byte) ([]byte, error) { + salt := payload[:saltLength] + key, err := encryptionKeyToBytes(secret, salt) + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // The IV needs to be unique, but not secure. Therefore it's common to + // include it at the beginning of the ciphertext. + if len(payload) < aes.BlockSize { + return nil, errors.New("payload too short") + } + iv := payload[saltLength : saltLength+aes.BlockSize] + payload = payload[saltLength+aes.BlockSize:] + payloadDst := make([]byte, len(payload)) + + stream := cipher.NewCFBDecrypter(block, iv) + + // XORKeyStream can work in-place if the two arguments are the same. + stream.XORKeyStream(payloadDst, payload) + return payloadDst, nil +} + +// Encrypt encrypts a payload with a given secret. +func encrypt(payload, secret []byte) ([]byte, error) { + salt, err := util.GetRandomString(saltLength) + if err != nil { + return nil, err + } + + key, err := encryptionKeyToBytes(secret, []byte(salt)) + if err != nil { + return nil, err + } + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // The IV needs to be unique, but not secure. Therefore it's common to + // include it at the beginning of the ciphertext. + ciphertext := make([]byte, saltLength+aes.BlockSize+len(payload)) + copy(ciphertext[:saltLength], salt) + iv := ciphertext[saltLength : saltLength+aes.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(ciphertext[saltLength+aes.BlockSize:], payload) + + return ciphertext, nil +} + +// Key needs to be 32bytes +func encryptionKeyToBytes(secret, salt []byte) ([]byte, error) { + return pbkdf2.Key(secret, salt, 10000, 32, sha256.New), nil +} diff --git a/pkg/services/secrets/secretkey.go b/pkg/services/secrets/secretkey.go index ced1f78465d..a37b0c64b2f 100644 --- a/pkg/services/secrets/secretkey.go +++ b/pkg/services/secrets/secretkey.go @@ -1,18 +1,5 @@ package secrets -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/sha256" - "errors" - "io" - - "golang.org/x/crypto/pbkdf2" - - "github.com/grafana/grafana/pkg/util" -) - type secretKey struct { key func() []byte } @@ -24,70 +11,3 @@ func (s *secretKey) Encrypt(blob []byte) ([]byte, error) { func (s *secretKey) Decrypt(blob []byte) ([]byte, error) { return decrypt(blob, s.key()) } - -const saltLength = 8 - -// Decrypt decrypts a payload with a given secret. -func decrypt(payload, secret []byte) ([]byte, error) { - salt := payload[:saltLength] - key, err := encryptionKeyToBytes(secret, salt) - if err != nil { - return nil, err - } - - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - // The IV needs to be unique, but not secure. Therefore it's common to - // include it at the beginning of the ciphertext. - if len(payload) < aes.BlockSize { - return nil, errors.New("payload too short") - } - iv := payload[saltLength : saltLength+aes.BlockSize] - payload = payload[saltLength+aes.BlockSize:] - payloadDst := make([]byte, len(payload)) - - stream := cipher.NewCFBDecrypter(block, iv) - - // XORKeyStream can work in-place if the two arguments are the same. - stream.XORKeyStream(payloadDst, payload) - return payloadDst, nil -} - -// Encrypt encrypts a payload with a given secret. -func encrypt(payload, secret []byte) ([]byte, error) { - salt, err := util.GetRandomString(saltLength) - if err != nil { - return nil, err - } - - key, err := encryptionKeyToBytes(secret, []byte(salt)) - if err != nil { - return nil, err - } - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - // The IV needs to be unique, but not secure. Therefore it's common to - // include it at the beginning of the ciphertext. - ciphertext := make([]byte, saltLength+aes.BlockSize+len(payload)) - copy(ciphertext[:saltLength], salt) - iv := ciphertext[saltLength : saltLength+aes.BlockSize] - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return nil, err - } - - stream := cipher.NewCFBEncrypter(block, iv) - stream.XORKeyStream(ciphertext[saltLength+aes.BlockSize:], payload) - - return ciphertext, nil -} - -// Key needs to be 32bytes -func encryptionKeyToBytes(secret, salt []byte) ([]byte, error) { - return pbkdf2.Key(secret, salt, 10000, 32, sha256.New), nil -} diff --git a/pkg/services/secrets/secrets.go b/pkg/services/secrets/secrets.go index d4a9abe3a30..dda57b19352 100644 --- a/pkg/services/secrets/secrets.go +++ b/pkg/services/secrets/secrets.go @@ -3,10 +3,18 @@ package secrets import ( "bytes" "context" + "crypto/rand" "encoding/base64" + "errors" "fmt" "time" + "github.com/grafana/grafana/pkg/util" + + "github.com/grafana/grafana/pkg/bus" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/infra/log" @@ -17,8 +25,10 @@ var logger = log.New("secrets") type Secrets struct { store *sqlstore.SqlStore `inject:""` + bus bus.Bus `inject:""` defaultEncryptionKey string + defaultProvider string providers map[string]Provider dataKeyCache map[string]dataKeyCacheItem } @@ -42,6 +52,46 @@ func (s *Secrets) Init() error { }, } + base_key := "root" + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _, err := s.store.GetDataKey(ctx, base_key) + if err != nil { + if errors.Is(err, models.ErrDataKeyNotFound) { + err = s.newRandomDataKey(ctx, base_key) + if err != nil { + return err + } + } else { + return err + } + } + + util.Encrypt = s.Encrypt + util.Decrypt = s.Decrypt + + return nil +} + +func (s *Secrets) newRandomDataKey(ctx context.Context, base_key string) error { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + return err + } + + encrypted, err := s.Encrypt(b) + if err != nil { + return err + } + + err = s.store.CreateDataKey(ctx, models.DataKey{ + Active: true, + Name: base_key, + Provider: s.defaultProvider, + EncryptedData: encrypted, + }) return nil } diff --git a/pkg/services/sqlstore/plugin_setting.go b/pkg/services/sqlstore/plugin_setting.go index a5ca6a7a0c9..5fd33d34866 100644 --- a/pkg/services/sqlstore/plugin_setting.go +++ b/pkg/services/sqlstore/plugin_setting.go @@ -5,7 +5,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -77,7 +76,7 @@ func UpdatePluginSetting(cmd *models.UpdatePluginSettingCmd) error { return err } for key, data := range cmd.SecureJsonData { - encryptedData, err := util.Encrypt([]byte(data), setting.SecretKey) + encryptedData, err := util.Encrypt([]byte(data)) if err != nil { return err } diff --git a/pkg/services/sqlstore/user_auth.go b/pkg/services/sqlstore/user_auth.go index ede3ae12451..677d3dfdff6 100644 --- a/pkg/services/sqlstore/user_auth.go +++ b/pkg/services/sqlstore/user_auth.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -265,7 +264,7 @@ func decodeAndDecrypt(s string) (string, error) { if err != nil { return "", err } - decrypted, err := util.Decrypt(decoded, setting.SecretKey) + decrypted, err := util.Decrypt(decoded) if err != nil { return "", err } @@ -275,7 +274,7 @@ func decodeAndDecrypt(s string) (string, error) { // encryptAndEncode will encrypt a string with grafana's secretKey, and // then encode it with the standard bas64 encoder func encryptAndEncode(s string) (string, error) { - encrypted, err := util.Encrypt([]byte(s), setting.SecretKey) + encrypted, err := util.Encrypt([]byte(s)) if err != nil { return "", err } diff --git a/pkg/util/encryption.go b/pkg/util/encryption.go index 6ecda766b47..9af1e1660d3 100644 --- a/pkg/util/encryption.go +++ b/pkg/util/encryption.go @@ -1,79 +1,17 @@ package util import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/sha256" "errors" - "io" - - "golang.org/x/crypto/pbkdf2" ) -const saltLength = 8 +var ErrNotInitialized = errors.New("function is not initialized") // Decrypt decrypts a payload with a given secret. -func Decrypt(payload []byte, secret string) ([]byte, error) { - salt := payload[:saltLength] - key, err := encryptionKeyToBytes(secret, string(salt)) - if err != nil { - return nil, err - } - - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - // The IV needs to be unique, but not secure. Therefore it's common to - // include it at the beginning of the ciphertext. - if len(payload) < aes.BlockSize { - return nil, errors.New("payload too short") - } - iv := payload[saltLength : saltLength+aes.BlockSize] - payload = payload[saltLength+aes.BlockSize:] - payloadDst := make([]byte, len(payload)) - - stream := cipher.NewCFBDecrypter(block, iv) - - // XORKeyStream can work in-place if the two arguments are the same. - stream.XORKeyStream(payloadDst, payload) - return payloadDst, nil +var Decrypt = func(_ []byte) ([]byte, error) { + return nil, ErrNotInitialized } // Encrypt encrypts a payload with a given secret. -func Encrypt(payload []byte, secret string) ([]byte, error) { - salt, err := GetRandomString(saltLength) - if err != nil { - return nil, err - } - - key, err := encryptionKeyToBytes(secret, salt) - if err != nil { - return nil, err - } - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - // The IV needs to be unique, but not secure. Therefore it's common to - // include it at the beginning of the ciphertext. - ciphertext := make([]byte, saltLength+aes.BlockSize+len(payload)) - copy(ciphertext[:saltLength], salt) - iv := ciphertext[saltLength : saltLength+aes.BlockSize] - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return nil, err - } - - stream := cipher.NewCFBEncrypter(block, iv) - stream.XORKeyStream(ciphertext[saltLength+aes.BlockSize:], payload) - - return ciphertext, nil -} - -// Key needs to be 32bytes -func encryptionKeyToBytes(secret, salt string) ([]byte, error) { - return pbkdf2.Key([]byte(secret), []byte(salt), 10000, 32, sha256.New), nil +var Encrypt = func(_ []byte) ([]byte, error) { + return nil, ErrNotInitialized }