Encryption: De-duplicate encryption code with extensible service (#52472)
* Encryption: De-duplicate encryption code with extensible service * Fix Wire injections * Fix tests * Register reload handler
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
encryptionprovider "github.com/grafana/grafana/pkg/services/encryption/provider"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func SetupTestService(tb testing.TB) *Service {
|
||||
tb.Helper()
|
||||
|
||||
usMock := &usagestats.UsageStatsMock{T: tb}
|
||||
provider := encryptionprovider.ProvideEncryptionProvider()
|
||||
settings := &setting.OSSImpl{Cfg: setting.NewCfg()}
|
||||
|
||||
service, err := ProvideEncryptionService(provider, usMock, settings)
|
||||
require.NoError(tb, err)
|
||||
|
||||
return service
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/services/encryption"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
encryptionAlgorithmDelimiter = '*'
|
||||
|
||||
securitySection = "security.encryption"
|
||||
encryptionAlgorithmKey = "algorithm"
|
||||
defaultEncryptionAlgorithm = encryption.AesCfb
|
||||
)
|
||||
|
||||
// Service must not be used for encryption.
|
||||
// Use secrets.Service implementing envelope encryption instead.
|
||||
type Service struct {
|
||||
log log.Logger
|
||||
|
||||
settingsProvider setting.Provider
|
||||
usageMetrics usagestats.Service
|
||||
|
||||
ciphers map[string]encryption.Cipher
|
||||
deciphers map[string]encryption.Decipher
|
||||
}
|
||||
|
||||
func ProvideEncryptionService(
|
||||
provider encryption.Provider,
|
||||
usageMetrics usagestats.Service,
|
||||
settingsProvider setting.Provider,
|
||||
) (*Service, error) {
|
||||
s := &Service{
|
||||
log: log.New("encryption"),
|
||||
|
||||
ciphers: provider.ProvideCiphers(),
|
||||
deciphers: provider.ProvideDeciphers(),
|
||||
|
||||
usageMetrics: usageMetrics,
|
||||
settingsProvider: settingsProvider,
|
||||
}
|
||||
|
||||
algorithm := s.settingsProvider.
|
||||
KeyValue(securitySection, encryptionAlgorithmKey).
|
||||
MustString(defaultEncryptionAlgorithm)
|
||||
|
||||
if err := s.checkEncryptionAlgorithm(algorithm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settingsProvider.RegisterReloadHandler(securitySection, s)
|
||||
|
||||
s.registerUsageMetrics()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Service) checkEncryptionAlgorithm(algorithm string) error {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
s.log.Error("Wrong security encryption configuration", "algorithm", algorithm, "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, ok := s.ciphers[algorithm]; !ok {
|
||||
err = errors.New("no cipher registered for encryption algorithm configured")
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := s.deciphers[algorithm]; !ok {
|
||||
err = errors.New("no cipher registered for encryption algorithm configured")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) registerUsageMetrics() {
|
||||
s.usageMetrics.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
|
||||
algorithm := s.settingsProvider.
|
||||
KeyValue(securitySection, encryptionAlgorithmKey).
|
||||
MustString(defaultEncryptionAlgorithm)
|
||||
|
||||
return map[string]interface{}{
|
||||
fmt.Sprintf("stats.encryption.%s.count", algorithm): 1,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Decrypt(ctx context.Context, payload []byte, secret string) ([]byte, error) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
s.log.Error("Decryption failed", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var (
|
||||
algorithm string
|
||||
toDecrypt []byte
|
||||
)
|
||||
algorithm, toDecrypt, err = deriveEncryptionAlgorithm(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decipher, ok := s.deciphers[algorithm]
|
||||
if !ok {
|
||||
err = fmt.Errorf("no decipher available for algorithm '%s'", algorithm)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var decrypted []byte
|
||||
decrypted, err = decipher.Decrypt(ctx, toDecrypt, secret)
|
||||
|
||||
return decrypted, err
|
||||
}
|
||||
|
||||
func deriveEncryptionAlgorithm(payload []byte) (string, []byte, error) {
|
||||
if len(payload) == 0 {
|
||||
return "", nil, fmt.Errorf("unable to derive encryption algorithm")
|
||||
}
|
||||
|
||||
if payload[0] != encryptionAlgorithmDelimiter {
|
||||
return encryption.AesCfb, payload, nil // backwards compatibility
|
||||
}
|
||||
|
||||
payload = payload[1:]
|
||||
algorithmDelimiterIdx := bytes.Index(payload, []byte{encryptionAlgorithmDelimiter})
|
||||
if algorithmDelimiterIdx == -1 {
|
||||
return encryption.AesCfb, payload, nil // backwards compatibility
|
||||
}
|
||||
|
||||
algorithmB64 := payload[:algorithmDelimiterIdx]
|
||||
payload = payload[algorithmDelimiterIdx+1:]
|
||||
|
||||
algorithm := make([]byte, base64.RawStdEncoding.DecodedLen(len(algorithmB64)))
|
||||
|
||||
_, err := base64.RawStdEncoding.Decode(algorithm, algorithmB64)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return string(algorithm), payload, nil
|
||||
}
|
||||
|
||||
func (s *Service) Encrypt(ctx context.Context, payload []byte, secret string) ([]byte, error) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
s.log.Error("Encryption failed", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
algorithm := s.settingsProvider.
|
||||
KeyValue(securitySection, encryptionAlgorithmKey).
|
||||
MustString(defaultEncryptionAlgorithm)
|
||||
|
||||
cipher, ok := s.ciphers[algorithm]
|
||||
if !ok {
|
||||
err = fmt.Errorf("no cipher available for algorithm '%s'", algorithm)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var encrypted []byte
|
||||
encrypted, err = cipher.Encrypt(ctx, payload, secret)
|
||||
|
||||
prefix := make([]byte, base64.RawStdEncoding.EncodedLen(len([]byte(algorithm)))+2)
|
||||
base64.RawStdEncoding.Encode(prefix[1:], []byte(algorithm))
|
||||
prefix[0] = encryptionAlgorithmDelimiter
|
||||
prefix[len(prefix)-1] = encryptionAlgorithmDelimiter
|
||||
|
||||
ciphertext := make([]byte, len(prefix)+len(encrypted))
|
||||
copy(ciphertext, prefix)
|
||||
copy(ciphertext[len(prefix):], encrypted)
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func (s *Service) EncryptJsonData(ctx context.Context, kv map[string]string, secret string) (map[string][]byte, error) {
|
||||
encrypted := make(map[string][]byte)
|
||||
for key, value := range kv {
|
||||
encryptedData, err := s.Encrypt(ctx, []byte(value), secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encrypted[key] = encryptedData
|
||||
}
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
func (s *Service) DecryptJsonData(ctx context.Context, sjd map[string][]byte, secret string) (map[string]string, error) {
|
||||
decrypted := make(map[string]string)
|
||||
for key, data := range sjd {
|
||||
decryptedData, err := s.Decrypt(ctx, data, secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decrypted[key] = string(decryptedData)
|
||||
}
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetDecryptedValue(ctx context.Context, sjd map[string][]byte, key, fallback, secret string) string {
|
||||
if value, ok := sjd[key]; ok {
|
||||
decryptedData, err := s.Decrypt(ctx, value, secret)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return string(decryptedData)
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
func (s *Service) Validate(section setting.Section) error {
|
||||
s.log.Debug("Validating encryption config")
|
||||
|
||||
algorithm := section.KeyValue(encryptionAlgorithmKey).
|
||||
MustString(defaultEncryptionAlgorithm)
|
||||
|
||||
if err := s.checkEncryptionAlgorithm(algorithm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Reload(_ setting.Section) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/services/encryption"
|
||||
"github.com/grafana/grafana/pkg/services/encryption/provider"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_Service(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
encProvider := provider.Provider{}
|
||||
usageStats := &usagestats.UsageStatsMock{}
|
||||
settings := &setting.OSSImpl{Cfg: setting.NewCfg()}
|
||||
|
||||
svc, err := ProvideEncryptionService(encProvider, usageStats, settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("decrypt empty payload should return error", func(t *testing.T) {
|
||||
_, err := svc.Decrypt(context.Background(), []byte(""), "1234")
|
||||
require.Error(t, err)
|
||||
|
||||
assert.Equal(t, "unable to derive encryption algorithm", err.Error())
|
||||
})
|
||||
|
||||
t.Run("encrypt and decrypt with aes-cfb should work", func(t *testing.T) {
|
||||
settings.Cfg.Raw.Section(securitySection).Key(encryptionAlgorithmKey).SetValue(encryption.AesCfb)
|
||||
|
||||
encrypted, err := svc.Encrypt(ctx, []byte("grafana"), "1234")
|
||||
require.NoError(t, err)
|
||||
|
||||
decrypted, err := svc.Decrypt(ctx, encrypted, "1234")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []byte("grafana"), decrypted)
|
||||
})
|
||||
|
||||
t.Run("decrypt with aes-gcm should work", func(t *testing.T) {
|
||||
// Raw slice of bytes that corresponds to the following ciphertext:
|
||||
// - 'grafana' as payload
|
||||
// - '1234' as secret
|
||||
// - 'aes-gcm' as encryption algorithm
|
||||
ciphertext := []byte{42, 89, 87, 86, 122, 76, 87, 100, 106, 98, 81, 42, 48, 99, 55, 50, 51, 48, 83, 66, 20, 99, 47, 238, 61, 44, 129, 125, 14, 37, 162, 230, 47, 31, 104, 70, 144, 223, 26, 51, 180, 17, 76, 52, 36, 93, 17, 203, 99, 158, 219, 102, 74, 173, 74}
|
||||
|
||||
decrypted, err := svc.Decrypt(context.Background(), ciphertext, "1234")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("grafana"), decrypted)
|
||||
})
|
||||
|
||||
t.Run("encrypt with aes-gcm should fail", func(t *testing.T) {
|
||||
settings.Cfg.Raw.Section(securitySection).Key(encryptionAlgorithmKey).SetValue(encryption.AesGcm)
|
||||
|
||||
_, err := svc.Encrypt(ctx, []byte("grafana"), "1234")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("decrypting legacy ciphertext should work", func(t *testing.T) {
|
||||
// Raw slice of bytes that corresponds to the following ciphertext:
|
||||
// - 'grafana' as payload
|
||||
// - '1234' as secret
|
||||
// - no encryption algorithm metadata
|
||||
ciphertext := []byte{73, 71, 50, 57, 121, 110, 90, 109, 115, 23, 237, 13, 130, 188, 151, 118, 98, 103, 80, 209, 79, 143, 22, 122, 44, 40, 102, 41, 136, 16, 27}
|
||||
|
||||
decrypted, err := svc.Decrypt(context.Background(), ciphertext, "1234")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("grafana"), decrypted)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Service_MissingProvider(t *testing.T) {
|
||||
encProvider := fakeProvider{}
|
||||
usageStats := &usagestats.UsageStatsMock{}
|
||||
settings := &setting.OSSImpl{Cfg: setting.NewCfg()}
|
||||
|
||||
service, err := ProvideEncryptionService(encProvider, usageStats, settings)
|
||||
assert.Nil(t, service)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
type fakeProvider struct{}
|
||||
|
||||
func (p fakeProvider) ProvideCiphers() map[string]encryption.Cipher {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p fakeProvider) ProvideDeciphers() map[string]encryption.Decipher {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user