SecretsManager: Add base encryption manager (#107562)

Co-authored-by: Michael Mandrus <michael.mandrus@grafana.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
This commit is contained in:
Dana Axinte
2025-07-03 11:29:14 +01:00
committed by GitHub
co-authored by Michael Mandrus Matheus Macabu
parent 93c14c52da
commit 4d8678c7f2
28 changed files with 1173 additions and 431 deletions
@@ -13,7 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher"
encryptionprovider "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/provider"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/provider"
"github.com/grafana/grafana/pkg/setting"
)
@@ -30,8 +30,9 @@ type Service struct {
cfg *setting.Cfg
usageMetrics usagestats.Service
ciphers map[string]cipher.Encrypter
deciphers map[string]cipher.Decrypter
cipher cipher.Encrypter
decipher cipher.Decrypter
algorithm string
}
func NewEncryptionService(
@@ -43,59 +44,29 @@ func NewEncryptionService(
return nil, fmt.Errorf("`[secrets_manager]secret_key` is not set")
}
if cfg.SecretsManagement.Encryption.Algorithm == "" {
return nil, fmt.Errorf("`[secrets_manager.encryption]algorithm` is not set")
}
s := &Service{
tracer: tracer,
log: log.New("encryption"),
ciphers: encryptionprovider.ProvideCiphers(),
deciphers: encryptionprovider.ProvideDeciphers(),
// Use the AES-GCM cipher for encryption and decryption.
// This is the only cipher supported by the secrets management system.
cipher: provider.NewAesGcmCipher(),
decipher: provider.NewAesGcmCipher(),
algorithm: provider.AesGcm,
usageMetrics: usageMetrics,
cfg: cfg,
}
algorithm := s.cfg.SecretsManagement.Encryption.Algorithm
if err := s.checkEncryptionAlgorithm(algorithm); err != nil {
return nil, err
}
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 = fmt.Errorf("no cipher registered for encryption algorithm '%s'", algorithm)
return err
}
if _, ok := s.deciphers[algorithm]; !ok {
err = fmt.Errorf("no decipher registered for encryption algorithm '%s'", algorithm)
return err
}
return nil
}
func (s *Service) registerUsageMetrics() {
s.usageMetrics.RegisterMetricsFunc(func(context.Context) (map[string]any, error) {
algorithm := s.cfg.SecretsManagement.Encryption.Algorithm
return map[string]any{
fmt.Sprintf("stats.%s.encryption.cipher.%s.count", encryption.UsageInsightsPrefix, algorithm): 1,
fmt.Sprintf("stats.%s.encryption.cipher.%s.count", encryption.UsageInsightsPrefix, s.algorithm): 1,
}, nil
})
}
@@ -120,16 +91,10 @@ func (s *Service) Decrypt(ctx context.Context, payload []byte, secret string) ([
return nil, err
}
decipher, ok := s.deciphers[algorithm]
if !ok {
err = fmt.Errorf("no decipher available for algorithm '%s'", algorithm)
return nil, err
}
span.SetAttributes(attribute.String("cipher.algorithm", algorithm))
var decrypted []byte
decrypted, err = decipher.Decrypt(ctx, toDecrypt, secret)
decrypted, err = s.decipher.Decrypt(ctx, toDecrypt, secret)
return decrypted, err
}
@@ -139,15 +104,8 @@ func (s *Service) deriveEncryptionAlgorithm(payload []byte) (string, []byte, err
return "", nil, fmt.Errorf("unable to derive encryption algorithm")
}
if payload[0] != encryptionAlgorithmDelimiter {
return cipher.AesCfb, payload, nil // backwards compatibility
}
payload = payload[1:]
algorithmDelimiterIdx := bytes.Index(payload, []byte{encryptionAlgorithmDelimiter})
if algorithmDelimiterIdx == -1 {
return cipher.AesCfb, payload, nil // backwards compatibility
}
algorithmB64 := payload[:algorithmDelimiterIdx]
payload = payload[algorithmDelimiterIdx+1:]
@@ -173,21 +131,13 @@ func (s *Service) Encrypt(ctx context.Context, payload []byte, secret string) ([
}
}()
algorithm := s.cfg.SecretsManagement.Encryption.Algorithm
cipher, ok := s.ciphers[algorithm]
if !ok {
err = fmt.Errorf("no cipher available for algorithm '%s'", algorithm)
return nil, err
}
span.SetAttributes(attribute.String("cipher.algorithm", algorithm))
span.SetAttributes(attribute.String("cipher.algorithm", s.algorithm))
var encrypted []byte
encrypted, err = cipher.Encrypt(ctx, payload, secret)
encrypted, err = s.cipher.Encrypt(ctx, payload, secret)
prefix := make([]byte, base64.RawStdEncoding.EncodedLen(len([]byte(algorithm)))+2)
base64.RawStdEncoding.Encode(prefix[1:], []byte(algorithm))
prefix := make([]byte, base64.RawStdEncoding.EncodedLen(len([]byte(s.algorithm)))+2)
base64.RawStdEncoding.Encode(prefix[1:], []byte(s.algorithm))
prefix[0] = encryptionAlgorithmDelimiter
prefix[len(prefix)-1] = encryptionAlgorithmDelimiter
@@ -2,14 +2,12 @@ package service
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher"
"github.com/grafana/grafana/pkg/setting"
)
@@ -21,11 +19,6 @@ func newGcmService(t *testing.T) *Service {
SecretsManagement: setting.SecretsManagerSettings{
SecretKey: "SdlklWklckeLS",
EncryptionProvider: "secretKey.v1",
Encryption: setting.EncryptionSettings{
DataKeysCacheTTL: 5 * time.Minute,
DataKeysCleanupInterval: 1 * time.Nanosecond,
Algorithm: cipher.AesGcm,
},
},
}
@@ -60,19 +53,4 @@ func TestService(t *testing.T) {
assert.Equal(t, []byte("grafana"), decrypted)
// We'll let the provider deal with testing details.
})
t.Run("decrypting legacy ciphertext should work", func(t *testing.T) {
t.Parallel()
// 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}
svc := newGcmService(t)
decrypted, err := svc.Decrypt(t.Context(), ciphertext, "1234")
require.NoError(t, err)
assert.Equal(t, []byte("grafana"), decrypted)
})
}