Skip to content

Commit a0108a1

Browse files
undef1ndjoanlopezsakjur
authored
Encryption: Extract encryption into service (grafana#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 <[email protected]> * Migrate to Wire * Undo non-desired changes * Move Encryption bindings to OSS Wire set Co-authored-by: Joan López de la Franca Beltran <[email protected]> Co-authored-by: Joan López de la Franca Beltran <[email protected]> Co-authored-by: Emil Tullstedt <[email protected]>
1 parent 79e79fe commit a0108a1

File tree

7 files changed

+161
-22
lines changed

7 files changed

+161
-22
lines changed

pkg/api/http_server.go

+12-11
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,6 @@ import (
1313
"strings"
1414
"sync"
1515

16-
"github.com/grafana/grafana/pkg/login/social"
17-
"github.com/grafana/grafana/pkg/services/cleanup"
18-
"github.com/grafana/grafana/pkg/services/ngalert"
19-
"github.com/grafana/grafana/pkg/services/notifications"
20-
21-
"github.com/grafana/grafana/pkg/services/libraryelements"
22-
"github.com/grafana/grafana/pkg/services/librarypanels"
23-
"github.com/grafana/grafana/pkg/services/oauthtoken"
24-
2516
"github.com/grafana/grafana/pkg/api/routing"
2617
httpstatic "github.com/grafana/grafana/pkg/api/static"
2718
"github.com/grafana/grafana/pkg/bus"
@@ -32,6 +23,7 @@ import (
3223
"github.com/grafana/grafana/pkg/infra/remotecache"
3324
"github.com/grafana/grafana/pkg/infra/tracing"
3425
"github.com/grafana/grafana/pkg/infra/usagestats"
26+
"github.com/grafana/grafana/pkg/login/social"
3527
"github.com/grafana/grafana/pkg/middleware"
3628
"github.com/grafana/grafana/pkg/models"
3729
"github.com/grafana/grafana/pkg/plugins"
@@ -40,13 +32,20 @@ import (
4032
"github.com/grafana/grafana/pkg/plugins/plugincontext"
4133
"github.com/grafana/grafana/pkg/services/accesscontrol"
4234
"github.com/grafana/grafana/pkg/services/alerting"
35+
"github.com/grafana/grafana/pkg/services/cleanup"
4336
"github.com/grafana/grafana/pkg/services/contexthandler"
4437
"github.com/grafana/grafana/pkg/services/datasourceproxy"
4538
"github.com/grafana/grafana/pkg/services/datasources"
39+
"github.com/grafana/grafana/pkg/services/encryption"
4640
"github.com/grafana/grafana/pkg/services/hooks"
41+
"github.com/grafana/grafana/pkg/services/libraryelements"
42+
"github.com/grafana/grafana/pkg/services/librarypanels"
4743
"github.com/grafana/grafana/pkg/services/live"
4844
"github.com/grafana/grafana/pkg/services/live/pushhttp"
4945
"github.com/grafana/grafana/pkg/services/login"
46+
"github.com/grafana/grafana/pkg/services/ngalert"
47+
"github.com/grafana/grafana/pkg/services/notifications"
48+
"github.com/grafana/grafana/pkg/services/oauthtoken"
5049
"github.com/grafana/grafana/pkg/services/provisioning"
5150
"github.com/grafana/grafana/pkg/services/quota"
5251
"github.com/grafana/grafana/pkg/services/rendering"
@@ -56,7 +55,6 @@ import (
5655
"github.com/grafana/grafana/pkg/services/sqlstore"
5756
"github.com/grafana/grafana/pkg/setting"
5857
"github.com/grafana/grafana/pkg/tsdb"
59-
6058
"github.com/grafana/grafana/pkg/util/errutil"
6159
"github.com/prometheus/client_golang/prometheus"
6260
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -107,6 +105,7 @@ type HTTPServer struct {
107105
SocialService social.Service
108106
OAuthTokenService oauthtoken.OAuthTokenService
109107
Listener net.Listener
108+
EncryptionService encryption.Service
110109
cleanUpService *cleanup.CleanUpService
111110
tracingService *tracing.TracingService
112111
internalMetricsSvc *metrics.InternalMetricsService
@@ -133,7 +132,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
133132
libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
134133
notificationService *notifications.NotificationService, tracingService *tracing.TracingService,
135134
internalMetricsSvc *metrics.InternalMetricsService, quotaService *quota.QuotaService,
136-
socialService social.Service, oauthTokenService oauthtoken.OAuthTokenService) (*HTTPServer, error) {
135+
socialService social.Service, oauthTokenService oauthtoken.OAuthTokenService,
136+
encryptionService encryption.Service) (*HTTPServer, error) {
137137
macaron.Env = cfg.Env
138138
m := macaron.New()
139139

@@ -180,6 +180,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
180180
Listener: opts.Listener,
181181
SocialService: socialService,
182182
OAuthTokenService: oauthTokenService,
183+
EncryptionService: encryptionService,
183184
}
184185
if hs.Listener != nil {
185186
hs.log.Debug("Using provided listener")

pkg/api/login.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"github.com/grafana/grafana/pkg/middleware/cookies"
1919
"github.com/grafana/grafana/pkg/models"
2020
"github.com/grafana/grafana/pkg/setting"
21-
"github.com/grafana/grafana/pkg/util"
2221
"github.com/grafana/grafana/pkg/util/errutil"
2322
)
2423

@@ -99,7 +98,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) {
9998
viewData.Settings["oauth"] = enabledOAuths
10099
viewData.Settings["samlEnabled"] = hs.samlEnabled()
101100

102-
if loginError, ok := tryGetEncryptedCookie(c, loginErrorCookieName); ok {
101+
if loginError, ok := hs.tryGetEncryptedCookie(c, loginErrorCookieName); ok {
103102
// this cookie is only set whenever an OAuth login fails
104103
// therefore the loginError should be passed to the view data
105104
// and the view should return immediately before attempting
@@ -299,7 +298,7 @@ func (hs *HTTPServer) Logout(c *models.ReqContext) {
299298
}
300299
}
301300

302-
func tryGetEncryptedCookie(ctx *models.ReqContext, cookieName string) (string, bool) {
301+
func (hs *HTTPServer) tryGetEncryptedCookie(ctx *models.ReqContext, cookieName string) (string, bool) {
303302
cookie := ctx.GetCookie(cookieName)
304303
if cookie == "" {
305304
return "", false
@@ -310,12 +309,12 @@ func tryGetEncryptedCookie(ctx *models.ReqContext, cookieName string) (string, b
310309
return "", false
311310
}
312311

313-
decryptedError, err := util.Decrypt(decoded, setting.SecretKey)
312+
decryptedError, err := hs.EncryptionService.Decrypt(decoded, setting.SecretKey)
314313
return string(decryptedError), err == nil
315314
}
316315

317316
func (hs *HTTPServer) trySetEncryptedCookie(ctx *models.ReqContext, cookieName string, value string, maxAge int) error {
318-
encryptedError, err := util.Encrypt([]byte(value), setting.SecretKey)
317+
encryptedError, err := hs.EncryptionService.Encrypt([]byte(value), setting.SecretKey)
319318
if err != nil {
320319
return err
321320
}

pkg/api/login_test.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"strings"
1111
"testing"
1212

13+
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
14+
1315
"github.com/grafana/grafana/pkg/api/dtos"
1416
"github.com/grafana/grafana/pkg/api/response"
1517
"github.com/grafana/grafana/pkg/api/routing"
@@ -23,7 +25,6 @@ import (
2325
"github.com/grafana/grafana/pkg/services/hooks"
2426
"github.com/grafana/grafana/pkg/services/licensing"
2527
"github.com/grafana/grafana/pkg/setting"
26-
"github.com/grafana/grafana/pkg/util"
2728
"github.com/stretchr/testify/assert"
2829
"github.com/stretchr/testify/require"
2930
)
@@ -105,10 +106,11 @@ func TestLoginErrorCookieAPIEndpoint(t *testing.T) {
105106
sc := setupScenarioContext(t, "/login")
106107
cfg := setting.NewCfg()
107108
hs := &HTTPServer{
108-
Cfg: cfg,
109-
SettingsProvider: &setting.OSSImpl{Cfg: cfg},
110-
License: &licensing.OSSLicensingService{},
111-
SocialService: &mockSocialService{},
109+
Cfg: cfg,
110+
SettingsProvider: &setting.OSSImpl{Cfg: cfg},
111+
License: &licensing.OSSLicensingService{},
112+
SocialService: &mockSocialService{},
113+
EncryptionService: &ossencryption.Service{},
112114
}
113115

114116
sc.defaultHandler = routing.Wrap(func(w http.ResponseWriter, c *models.ReqContext) {
@@ -121,7 +123,7 @@ func TestLoginErrorCookieAPIEndpoint(t *testing.T) {
121123
setting.OAuthAutoLogin = true
122124

123125
oauthError := errors.New("User not a member of one of the required organizations")
124-
encryptedError, err := util.Encrypt([]byte(oauthError.Error()), setting.SecretKey)
126+
encryptedError, err := hs.EncryptionService.Encrypt([]byte(oauthError.Error()), setting.SecretKey)
125127
require.NoError(t, err)
126128
expCookiePath := "/"
127129
if len(setting.AppSubUrl) > 0 {

pkg/server/wireexts_oss.go

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
1212
"github.com/grafana/grafana/pkg/services/auth"
1313
"github.com/grafana/grafana/pkg/services/datasources"
14+
"github.com/grafana/grafana/pkg/services/encryption"
15+
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
1416
"github.com/grafana/grafana/pkg/services/licensing"
1517
"github.com/grafana/grafana/pkg/services/login"
1618
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
@@ -43,6 +45,8 @@ var wireExtsBasicSet = wire.NewSet(
4345
wire.Bind(new(registry.DatabaseMigrator), new(*migrations.OSSMigrations)),
4446
authinfoservice.ProvideOSSUserProtectionService,
4547
wire.Bind(new(login.UserProtectionService), new(*authinfoservice.OSSUserProtectionImpl)),
48+
ossencryption.ProvideService,
49+
wire.Bind(new(encryption.Service), new(*ossencryption.Service)),
4650
)
4751

4852
var wireExtsSet = wire.NewSet(

pkg/services/encryption/encryption.go

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package encryption
2+
3+
type Service interface {
4+
Encrypt([]byte, string) ([]byte, error)
5+
Decrypt([]byte, string) ([]byte, error)
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package ossencryption
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"crypto/rand"
7+
"crypto/sha256"
8+
"errors"
9+
"fmt"
10+
"io"
11+
12+
"github.com/grafana/grafana/pkg/util"
13+
"golang.org/x/crypto/pbkdf2"
14+
)
15+
16+
type Service struct{}
17+
18+
func ProvideService() *Service {
19+
return &Service{}
20+
}
21+
22+
const saltLength = 8
23+
24+
func (s *Service) Decrypt(payload []byte, secret string) ([]byte, error) {
25+
if len(payload) < saltLength {
26+
return nil, fmt.Errorf("unable to compute salt")
27+
}
28+
salt := payload[:saltLength]
29+
key, err := encryptionKeyToBytes(secret, string(salt))
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
block, err := aes.NewCipher(key)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
// The IV needs to be unique, but not secure. Therefore it's common to
40+
// include it at the beginning of the ciphertext.
41+
if len(payload) < aes.BlockSize {
42+
return nil, errors.New("payload too short")
43+
}
44+
iv := payload[saltLength : saltLength+aes.BlockSize]
45+
payload = payload[saltLength+aes.BlockSize:]
46+
payloadDst := make([]byte, len(payload))
47+
48+
stream := cipher.NewCFBDecrypter(block, iv)
49+
50+
// XORKeyStream can work in-place if the two arguments are the same.
51+
stream.XORKeyStream(payloadDst, payload)
52+
return payloadDst, nil
53+
}
54+
55+
func (s *Service) Encrypt(payload []byte, secret string) ([]byte, error) {
56+
salt, err := util.GetRandomString(saltLength)
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
key, err := encryptionKeyToBytes(secret, salt)
62+
if err != nil {
63+
return nil, err
64+
}
65+
block, err := aes.NewCipher(key)
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
// The IV needs to be unique, but not secure. Therefore it's common to
71+
// include it at the beginning of the ciphertext.
72+
ciphertext := make([]byte, saltLength+aes.BlockSize+len(payload))
73+
copy(ciphertext[:saltLength], salt)
74+
iv := ciphertext[saltLength : saltLength+aes.BlockSize]
75+
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
76+
return nil, err
77+
}
78+
79+
stream := cipher.NewCFBEncrypter(block, iv)
80+
stream.XORKeyStream(ciphertext[saltLength+aes.BlockSize:], payload)
81+
82+
return ciphertext, nil
83+
}
84+
85+
// Key needs to be 32bytes
86+
func encryptionKeyToBytes(secret, salt string) ([]byte, error) {
87+
return pbkdf2.Key([]byte(secret), []byte(salt), 10000, 32, sha256.New), nil
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package ossencryption
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestEncryption(t *testing.T) {
11+
svc := Service{}
12+
13+
t.Run("getting encryption key", func(t *testing.T) {
14+
key, err := encryptionKeyToBytes("secret", "salt")
15+
require.NoError(t, err)
16+
assert.Len(t, key, 32)
17+
18+
key, err = encryptionKeyToBytes("a very long secret key that is larger then 32bytes", "salt")
19+
require.NoError(t, err)
20+
assert.Len(t, key, 32)
21+
})
22+
23+
t.Run("decrypting basic payload", func(t *testing.T) {
24+
encrypted, err := svc.Encrypt([]byte("grafana"), "1234")
25+
require.NoError(t, err)
26+
27+
decrypted, err := svc.Decrypt(encrypted, "1234")
28+
require.NoError(t, err)
29+
30+
assert.Equal(t, []byte("grafana"), decrypted)
31+
})
32+
33+
t.Run("decrypting empty payload should return error", func(t *testing.T) {
34+
_, err := svc.Decrypt([]byte(""), "1234")
35+
require.Error(t, err)
36+
37+
assert.Equal(t, "unable to compute salt", err.Error())
38+
})
39+
}

0 commit comments

Comments
 (0)