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:
Joan López de la Franca Beltran
2022-08-02 15:08:09 +02:00
committed by GitHub
parent 9c6aab3bc9
commit 28e27e1365
41 changed files with 809 additions and 367 deletions
@@ -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
}
+243
View File
@@ -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
}