mirror of https://github.com/grafana/grafana
Encryption: Extract encryption into service (#38442)
* Add encryption service * Add tests for encryption service * Inject encryption service into http server * Replace encryption global function usage in login tests * Apply suggestions from code review Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> * Migrate to Wire * Undo non-desired changes * Move Encryption bindings to OSS Wire set Co-authored-by: Joan López de la Franca Beltran <joanjan14@gmail.com> Co-authored-by: Joan López de la Franca Beltran <5459617+joanlopez@users.noreply.github.com> Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>pull/38730/head
parent
79e79fe244
commit
a0108a1e5b
@ -0,0 +1,6 @@ |
||||
package encryption |
||||
|
||||
type Service interface { |
||||
Encrypt([]byte, string) ([]byte, error) |
||||
Decrypt([]byte, string) ([]byte, error) |
||||
} |
@ -0,0 +1,88 @@ |
||||
package ossencryption |
||||
|
||||
import ( |
||||
"crypto/aes" |
||||
"crypto/cipher" |
||||
"crypto/rand" |
||||
"crypto/sha256" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
|
||||
"github.com/grafana/grafana/pkg/util" |
||||
"golang.org/x/crypto/pbkdf2" |
||||
) |
||||
|
||||
type Service struct{} |
||||
|
||||
func ProvideService() *Service { |
||||
return &Service{} |
||||
} |
||||
|
||||
const saltLength = 8 |
||||
|
||||
func (s *Service) Decrypt(payload []byte, secret string) ([]byte, error) { |
||||
if len(payload) < saltLength { |
||||
return nil, fmt.Errorf("unable to compute salt") |
||||
} |
||||
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 |
||||
} |
||||
|
||||
func (s *Service) Encrypt(payload []byte, secret string) ([]byte, error) { |
||||
salt, err := util.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 |
||||
} |
@ -0,0 +1,39 @@ |
||||
package ossencryption |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestEncryption(t *testing.T) { |
||||
svc := Service{} |
||||
|
||||
t.Run("getting encryption key", func(t *testing.T) { |
||||
key, err := encryptionKeyToBytes("secret", "salt") |
||||
require.NoError(t, err) |
||||
assert.Len(t, key, 32) |
||||
|
||||
key, err = encryptionKeyToBytes("a very long secret key that is larger then 32bytes", "salt") |
||||
require.NoError(t, err) |
||||
assert.Len(t, key, 32) |
||||
}) |
||||
|
||||
t.Run("decrypting basic payload", func(t *testing.T) { |
||||
encrypted, err := svc.Encrypt([]byte("grafana"), "1234") |
||||
require.NoError(t, err) |
||||
|
||||
decrypted, err := svc.Decrypt(encrypted, "1234") |
||||
require.NoError(t, err) |
||||
|
||||
assert.Equal(t, []byte("grafana"), decrypted) |
||||
}) |
||||
|
||||
t.Run("decrypting empty payload should return error", func(t *testing.T) { |
||||
_, err := svc.Decrypt([]byte(""), "1234") |
||||
require.Error(t, err) |
||||
|
||||
assert.Equal(t, "unable to compute salt", err.Error()) |
||||
}) |
||||
} |
Loading…
Reference in new issue