diff --git a/pkg/services/encryption/ossencryption/ossencryption.go b/pkg/services/encryption/ossencryption/ossencryption.go index 60168b48e9c..bfce3e35af8 100644 --- a/pkg/services/encryption/ossencryption/ossencryption.go +++ b/pkg/services/encryption/ossencryption/ossencryption.go @@ -1,11 +1,13 @@ package ossencryption import ( + "bytes" "context" "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" + "encoding/base64" "errors" "fmt" "io" @@ -22,11 +24,20 @@ func ProvideService() *Service { return &Service{} } -const saltLength = 8 +const ( + saltLength = 8 + aesCfb = "aes-cfb" + encryptionAlgorithmDelimiter = '*' +) func (s *Service) Decrypt(_ context.Context, payload []byte, secret string) ([]byte, error) { + alg, payload, err := deriveEncryptionAlgorithm(payload) + if err != nil { + return nil, err + } + if len(payload) < saltLength { - return nil, fmt.Errorf("unable to compute salt") + return nil, errors.New("unable to compute salt") } salt := payload[:saltLength] key, err := encryptionKeyToBytes(secret, string(salt)) @@ -39,11 +50,49 @@ func (s *Service) Decrypt(_ context.Context, payload []byte, secret string) ([]b return nil, err } - // The IV needs to be unique, but not secure. Therefore it's common to + switch alg { + case aesCfb: + return decryptCFB(block, payload) + default: + return nil, errors.New("unsupported encryption algorithm") + } +} + +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 aesCfb, payload, nil // backwards compatibility + } + + payload = payload[1:] + algDelim := bytes.Index(payload, []byte{encryptionAlgorithmDelimiter}) + if algDelim == -1 { + return aesCfb, payload, nil // backwards compatibility + } + + algB64 := payload[:algDelim] + payload = payload[algDelim+1:] + + alg := make([]byte, base64.RawStdEncoding.DecodedLen(len(algB64))) + + _, err := base64.RawStdEncoding.Decode(alg, algB64) + if err != nil { + return "", nil, err + } + + return string(alg), payload, nil +} + +func decryptCFB(block cipher.Block, payload []byte) ([]byte, error) { + // 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)) @@ -65,6 +114,7 @@ func (s *Service) Encrypt(_ context.Context, payload []byte, secret string) ([]b if err != nil { return nil, err } + block, err := aes.NewCipher(key) if err != nil { return nil, err diff --git a/pkg/services/encryption/ossencryption/ossencryption_test.go b/pkg/services/encryption/ossencryption/ossencryption_test.go index ad61f4ab0f2..c3ebcda536f 100644 --- a/pkg/services/encryption/ossencryption/ossencryption_test.go +++ b/pkg/services/encryption/ossencryption/ossencryption_test.go @@ -37,6 +37,32 @@ func TestEncryption(t *testing.T) { _, err := svc.Decrypt(context.Background(), []byte(""), "1234") require.Error(t, err) - assert.Equal(t, "unable to compute salt", err.Error()) + assert.Equal(t, "unable to derive encryption algorithm", err.Error()) + }) + + t.Run("decrypting ciphertext with aes-gcm as encryption algorithm should return error", 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 + // With no encryption algorithm metadata. + 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} + _, err := svc.Decrypt(context.Background(), ciphertext, "1234") + require.Error(t, err) + + assert.Equal(t, "unsupported encryption algorithm", err.Error()) + }) + + t.Run("decrypting ciphertext with aes-cfb as encryption algorithm do not fail", func(t *testing.T) { + // Raw slice of bytes that corresponds to the following ciphertext: + // - 'grafana' as payload + // - '1234' as secret + // - 'aes-cfb' as encryption algorithm + // With no encryption algorithm metadata. + ciphertext := []byte{42, 89, 87, 86, 122, 76, 87, 78, 109, 89, 103, 42, 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) }) }