From ab3b586838578d84a0c3495c84d9dd8efe28f776 Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Sat, 23 Jan 2016 03:15:39 +0800 Subject: [PATCH 1/6] add encryption util functions --- pkg/util/encryption.go | 70 +++++++++++++++++++++++++++++++++++++ pkg/util/encryption_test.go | 28 +++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 pkg/util/encryption.go create mode 100644 pkg/util/encryption_test.go diff --git a/pkg/util/encryption.go b/pkg/util/encryption.go new file mode 100644 index 00000000000..f24a9b29397 --- /dev/null +++ b/pkg/util/encryption.go @@ -0,0 +1,70 @@ +package util + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "io" + + "github.com/grafana/grafana/pkg/log" +) + +func Decrypt(payload []byte, secret string) []byte { + key := encryptionKeyToBytes(secret) + + block, err := aes.NewCipher(key) + if err != nil { + log.Fatal(4, err.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 { + log.Fatal(4, "payload too short") + } + iv := payload[:aes.BlockSize] + payload = payload[aes.BlockSize:] + + stream := cipher.NewCFBDecrypter(block, iv) + + // XORKeyStream can work in-place if the two arguments are the same. + stream.XORKeyStream(payload, payload) + return payload +} + +func Encrypt(payload []byte, secret string) []byte { + key := encryptionKeyToBytes(secret) + + block, err := aes.NewCipher(key) + if err != nil { + log.Fatal(4, err.Error()) + } + + // 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, aes.BlockSize+len(payload)) + iv := ciphertext[:aes.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + log.Fatal(4, err.Error()) + } + + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(ciphertext[aes.BlockSize:], payload) + + return ciphertext +} + +// Key needs to be 32bytes +func encryptionKeyToBytes(secret string) []byte { + key := make([]byte, 32, 32) + keyBytes := []byte(secret) + secretLength := len(keyBytes) + for i := 0; i < 32; i++ { + if secretLength > i { + key[i] = keyBytes[i] + } else { + key[i] = 0 + } + } + return key +} diff --git a/pkg/util/encryption_test.go b/pkg/util/encryption_test.go new file mode 100644 index 00000000000..254f0f178c0 --- /dev/null +++ b/pkg/util/encryption_test.go @@ -0,0 +1,28 @@ +package util + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestEncryption(t *testing.T) { + + Convey("When getting encryption key", t, func() { + + key := encryptionKeyToBytes("secret") + So(len(key), ShouldEqual, 32) + + key = encryptionKeyToBytes("a very long secret key that is larger then 32bytes") + So(len(key), ShouldEqual, 32) + + }) + + Convey("When decrypting basic payload", t, func() { + encrypted := Encrypt([]byte("grafana"), "1234") + decrypted := Decrypt(encrypted, "1234") + + So(string(decrypted), ShouldEqual, "grafana") + }) + +} From 32f78d465bb1bd8d393db4bf2624cf8845345a7d Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Sat, 23 Jan 2016 06:17:22 +0800 Subject: [PATCH 2/6] add secureJsonData to appSettings model. - adds the new column to the DB table. - data stored in the DB is encrypted - update appRouteHeaders templates to use the jsonData and decrypted secureJsonData --- pkg/api/app_routes.go | 11 +++++-- pkg/models/app_settings.go | 33 ++++++++++++++----- pkg/services/sqlstore/app_settings.go | 25 ++++++++++---- .../sqlstore/migrations/app_settings.go | 1 + public/app/features/apps/edit_ctrl.ts | 1 + 5 files changed, 53 insertions(+), 18 deletions(-) diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index 6ad41f79b53..169c5c6d15c 100644 --- a/pkg/api/app_routes.go +++ b/pkg/api/app_routes.go @@ -94,8 +94,15 @@ func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins ctx.JsonApiErr(500, "failed to get AppSettings.", err) return } - - err = t.Execute(&contentBuf, query.Result.JsonData) + type templateData struct { + JsonData map[string]interface{} + SecureJsonData map[string]string + } + data := templateData{ + JsonData: query.Result.JsonData, + SecureJsonData: query.Result.SecureJsonData.Decrypt(), + } + err = t.Execute(&contentBuf, data) if err != nil { ctx.JsonApiErr(500, fmt.Sprintf("failed to execute header content template for header %s.", header.Name), err) return diff --git a/pkg/models/app_settings.go b/pkg/models/app_settings.go index f3b60502cb0..78d4c483f2b 100644 --- a/pkg/models/app_settings.go +++ b/pkg/models/app_settings.go @@ -3,6 +3,9 @@ package models import ( "errors" "time" + + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) var ( @@ -10,25 +13,37 @@ var ( ) type AppSettings struct { - Id int64 - AppId string - OrgId int64 - Enabled bool - Pinned bool - JsonData map[string]interface{} + Id int64 + AppId string + OrgId int64 + Enabled bool + Pinned bool + JsonData map[string]interface{} + SecureJsonData SecureJsonData Created time.Time Updated time.Time } +type SecureJsonData map[string][]byte + +func (s SecureJsonData) Decrypt() map[string]string { + decrypted := make(map[string]string) + for key, data := range s { + decrypted[key] = string(util.Decrypt(data, setting.SecretKey)) + } + return decrypted +} + // ---------------------- // COMMANDS // Also acts as api DTO type UpdateAppSettingsCmd struct { - Enabled bool `json:"enabled"` - Pinned bool `json:"pinned"` - JsonData map[string]interface{} `json:"jsonData"` + Enabled bool `json:"enabled"` + Pinned bool `json:"pinned"` + JsonData map[string]interface{} `json:"jsonData"` + SecureJsonData map[string]string `json:"secureJsonData"` AppId string `json:"-"` OrgId int64 `json:"-"` diff --git a/pkg/services/sqlstore/app_settings.go b/pkg/services/sqlstore/app_settings.go index 7d9482e7b22..f454d2cc5ff 100644 --- a/pkg/services/sqlstore/app_settings.go +++ b/pkg/services/sqlstore/app_settings.go @@ -5,6 +5,8 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) func init() { @@ -40,18 +42,27 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error { sess.UseBool("enabled") sess.UseBool("pinned") if !exists { + // encrypt secureJsonData + secureJsonData := make(map[string][]byte) + for key, data := range cmd.SecureJsonData { + secureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey) + } app = m.AppSettings{ - AppId: cmd.AppId, - OrgId: cmd.OrgId, - Enabled: cmd.Enabled, - Pinned: cmd.Pinned, - JsonData: cmd.JsonData, - Created: time.Now(), - Updated: time.Now(), + AppId: cmd.AppId, + OrgId: cmd.OrgId, + Enabled: cmd.Enabled, + Pinned: cmd.Pinned, + JsonData: cmd.JsonData, + SecureJsonData: secureJsonData, + Created: time.Now(), + Updated: time.Now(), } _, err = sess.Insert(&app) return err } else { + for key, data := range cmd.SecureJsonData { + app.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey) + } app.Updated = time.Now() app.Enabled = cmd.Enabled app.JsonData = cmd.JsonData diff --git a/pkg/services/sqlstore/migrations/app_settings.go b/pkg/services/sqlstore/migrations/app_settings.go index 437debbe95b..8b970a5062a 100644 --- a/pkg/services/sqlstore/migrations/app_settings.go +++ b/pkg/services/sqlstore/migrations/app_settings.go @@ -13,6 +13,7 @@ func addAppSettingsMigration(mg *Migrator) { {Name: "enabled", Type: DB_Bool, Nullable: false}, {Name: "pinned", Type: DB_Bool, Nullable: false}, {Name: "json_data", Type: DB_Text, Nullable: true}, + {Name: "secure_json_data", Type: DB_Text, Nullable: true}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, }, diff --git a/public/app/features/apps/edit_ctrl.ts b/public/app/features/apps/edit_ctrl.ts index dfbce64df5e..ccdaf529b5e 100644 --- a/public/app/features/apps/edit_ctrl.ts +++ b/public/app/features/apps/edit_ctrl.ts @@ -24,6 +24,7 @@ export class AppEditCtrl { enabled: this.appModel.enabled, pinned: this.appModel.pinned, jsonData: this.appModel.jsonData, + secureJsonData: this.appModel.secureJsonData, }, options); this.backendSrv.post(`/api/org/apps/${this.$routeParams.appId}/settings`, updateCmd).then(function() { From 40d946a6e3399f05ccd9cf654debfa7f2eb02786 Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Tue, 26 Jan 2016 04:18:18 +0800 Subject: [PATCH 3/6] add drop table to ensure existing installs get new schema --- pkg/services/sqlstore/migrations/app_settings.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/migrations/app_settings.go b/pkg/services/sqlstore/migrations/app_settings.go index 8b970a5062a..9c01b242b6e 100644 --- a/pkg/services/sqlstore/migrations/app_settings.go +++ b/pkg/services/sqlstore/migrations/app_settings.go @@ -4,7 +4,7 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" func addAppSettingsMigration(mg *Migrator) { - appSettingsV1 := Table{ + appSettingsV2 := Table{ Name: "app_settings", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, @@ -22,7 +22,9 @@ func addAppSettingsMigration(mg *Migrator) { }, } - mg.AddMigration("create app_settings table v1", NewAddTableMigration(appSettingsV1)) + mg.AddMigration("Drop old table app_settings v1", NewDropTableMigration("app_settings")) + + mg.AddMigration("create app_settings table v2", NewAddTableMigration(appSettingsV2)) //------- indexes ------------------ addTableIndicesMigrations(mg, "v3", appSettingsV1) From 092bb69c416e3b22536a01e5bdae9671ad18651e Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Tue, 26 Jan 2016 04:18:44 +0800 Subject: [PATCH 4/6] instead of padding with 0's, cycle through the secret. --- pkg/util/encryption.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/util/encryption.go b/pkg/util/encryption.go index f24a9b29397..ee0f188f04d 100644 --- a/pkg/util/encryption.go +++ b/pkg/util/encryption.go @@ -60,11 +60,7 @@ func encryptionKeyToBytes(secret string) []byte { keyBytes := []byte(secret) secretLength := len(keyBytes) for i := 0; i < 32; i++ { - if secretLength > i { - key[i] = keyBytes[i] - } else { - key[i] = 0 - } + key[i] = keyBytes[i%secretLength] } return key } From 05868bc1dfb28a2fd36792b6259ba3ba7951fefb Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Tue, 26 Jan 2016 04:24:44 +0800 Subject: [PATCH 5/6] fix typo --- pkg/services/sqlstore/migrations/app_settings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/migrations/app_settings.go b/pkg/services/sqlstore/migrations/app_settings.go index 9c01b242b6e..885dbbf9f05 100644 --- a/pkg/services/sqlstore/migrations/app_settings.go +++ b/pkg/services/sqlstore/migrations/app_settings.go @@ -27,5 +27,5 @@ func addAppSettingsMigration(mg *Migrator) { mg.AddMigration("create app_settings table v2", NewAddTableMigration(appSettingsV2)) //------- indexes ------------------ - addTableIndicesMigrations(mg, "v3", appSettingsV1) + addTableIndicesMigrations(mg, "v3", appSettingsV2) } From c8c337ceadc1ef97114cce30828800b6cb6bd842 Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Tue, 26 Jan 2016 05:15:29 +0800 Subject: [PATCH 6/6] use PBKDF2 to esnure key is 23bytes. --- pkg/util/encryption.go | 30 +++++++++++++++--------------- pkg/util/encryption_test.go | 5 ++--- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/pkg/util/encryption.go b/pkg/util/encryption.go index ee0f188f04d..42586ddac8b 100644 --- a/pkg/util/encryption.go +++ b/pkg/util/encryption.go @@ -4,13 +4,17 @@ import ( "crypto/aes" "crypto/cipher" "crypto/rand" + "crypto/sha256" "io" "github.com/grafana/grafana/pkg/log" ) +const saltLength = 8 + func Decrypt(payload []byte, secret string) []byte { - key := encryptionKeyToBytes(secret) + salt := payload[:saltLength] + key := encryptionKeyToBytes(secret, string(salt)) block, err := aes.NewCipher(key) if err != nil { @@ -22,8 +26,8 @@ func Decrypt(payload []byte, secret string) []byte { if len(payload) < aes.BlockSize { log.Fatal(4, "payload too short") } - iv := payload[:aes.BlockSize] - payload = payload[aes.BlockSize:] + iv := payload[saltLength : saltLength+aes.BlockSize] + payload = payload[saltLength+aes.BlockSize:] stream := cipher.NewCFBDecrypter(block, iv) @@ -33,8 +37,9 @@ func Decrypt(payload []byte, secret string) []byte { } func Encrypt(payload []byte, secret string) []byte { - key := encryptionKeyToBytes(secret) + salt := GetRandomString(saltLength) + key := encryptionKeyToBytes(secret, salt) block, err := aes.NewCipher(key) if err != nil { log.Fatal(4, err.Error()) @@ -42,25 +47,20 @@ func Encrypt(payload []byte, secret string) []byte { // 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, aes.BlockSize+len(payload)) - iv := ciphertext[:aes.BlockSize] + ciphertext := make([]byte, saltLength+aes.BlockSize+len(payload)) + copy(ciphertext[:saltLength], []byte(salt)) + iv := ciphertext[saltLength : saltLength+aes.BlockSize] if _, err := io.ReadFull(rand.Reader, iv); err != nil { log.Fatal(4, err.Error()) } stream := cipher.NewCFBEncrypter(block, iv) - stream.XORKeyStream(ciphertext[aes.BlockSize:], payload) + stream.XORKeyStream(ciphertext[saltLength+aes.BlockSize:], payload) return ciphertext } // Key needs to be 32bytes -func encryptionKeyToBytes(secret string) []byte { - key := make([]byte, 32, 32) - keyBytes := []byte(secret) - secretLength := len(keyBytes) - for i := 0; i < 32; i++ { - key[i] = keyBytes[i%secretLength] - } - return key +func encryptionKeyToBytes(secret, salt string) []byte { + return PBKDF2([]byte(secret), []byte(salt), 10000, 32, sha256.New) } diff --git a/pkg/util/encryption_test.go b/pkg/util/encryption_test.go index 254f0f178c0..5f1dc18fea3 100644 --- a/pkg/util/encryption_test.go +++ b/pkg/util/encryption_test.go @@ -10,12 +10,11 @@ func TestEncryption(t *testing.T) { Convey("When getting encryption key", t, func() { - key := encryptionKeyToBytes("secret") + key := encryptionKeyToBytes("secret", "salt") So(len(key), ShouldEqual, 32) - key = encryptionKeyToBytes("a very long secret key that is larger then 32bytes") + key = encryptionKeyToBytes("a very long secret key that is larger then 32bytes", "salt") So(len(key), ShouldEqual, 32) - }) Convey("When decrypting basic payload", t, func() {