|
|
05d305 |
From 5749f50533225b5d38fed1ed86b1c893cc0466b5 Mon Sep 17 00:00:00 2001
|
|
|
05d305 |
From: Andreas Gerstmayr <agerstmayr@redhat.com>
|
|
|
05d305 |
Date: Thu, 25 Nov 2021 18:49:52 +0100
|
|
|
05d305 |
Subject: [PATCH] notifications: use HMAC-SHA256 to generate password reset
|
|
|
05d305 |
tokens
|
|
|
b83884 |
|
|
|
05d305 |
* changes the time limit code generation function to use HMAC-SHA256
|
|
|
05d305 |
instead of SHA-1
|
|
|
05d305 |
* multiple new testcases
|
|
|
b83884 |
|
|
|
b83884 |
diff --git a/pkg/services/notifications/codes.go b/pkg/services/notifications/codes.go
|
|
|
05d305 |
index 32cd5dd7cd..72d33e3814 100644
|
|
|
b83884 |
--- a/pkg/services/notifications/codes.go
|
|
|
b83884 |
+++ b/pkg/services/notifications/codes.go
|
|
|
b83884 |
@@ -1,48 +1,53 @@
|
|
|
b83884 |
package notifications
|
|
|
b83884 |
|
|
|
b83884 |
import (
|
|
|
b83884 |
- "crypto/sha1" // #nosec
|
|
|
b83884 |
+ "crypto/hmac"
|
|
|
b83884 |
+ "crypto/sha256"
|
|
|
b83884 |
"encoding/hex"
|
|
|
b83884 |
"fmt"
|
|
|
b83884 |
+ "strconv"
|
|
|
b83884 |
"time"
|
|
|
b83884 |
|
|
|
b83884 |
- "github.com/unknwon/com"
|
|
|
b83884 |
-
|
|
|
b83884 |
"github.com/grafana/grafana/pkg/models"
|
|
|
b83884 |
"github.com/grafana/grafana/pkg/setting"
|
|
|
b83884 |
)
|
|
|
b83884 |
|
|
|
b83884 |
-const timeLimitCodeLength = 12 + 6 + 40
|
|
|
b83884 |
+const timeLimitStartDateLength = 12
|
|
|
b83884 |
+const timeLimitMinutesLength = 6
|
|
|
b83884 |
+const timeLimitHmacLength = 64
|
|
|
b83884 |
+const timeLimitCodeLength = timeLimitStartDateLength + timeLimitMinutesLength + timeLimitHmacLength
|
|
|
b83884 |
|
|
|
b83884 |
// create a time limit code
|
|
|
b83884 |
-// code format: 12 length date time string + 6 minutes string + 40 sha1 encoded string
|
|
|
b83884 |
-func createTimeLimitCode(data string, minutes int, startInf interface{}) (string, error) {
|
|
|
b83884 |
+// code format: 12 length date time string + 6 minutes string + 64 HMAC-SHA256 encoded string
|
|
|
b83884 |
+func createTimeLimitCode(payload string, minutes int, startStr string) (string, error) {
|
|
|
b83884 |
format := "200601021504"
|
|
|
b83884 |
|
|
|
b83884 |
var start, end time.Time
|
|
|
b83884 |
- var startStr, endStr string
|
|
|
b83884 |
+ var endStr string
|
|
|
b83884 |
|
|
|
b83884 |
- if startInf == nil {
|
|
|
b83884 |
+ if startStr == "" {
|
|
|
b83884 |
// Use now time create code
|
|
|
b83884 |
start = time.Now()
|
|
|
b83884 |
startStr = start.Format(format)
|
|
|
b83884 |
} else {
|
|
|
b83884 |
// use start string create code
|
|
|
b83884 |
- startStr = startInf.(string)
|
|
|
b83884 |
- start, _ = time.ParseInLocation(format, startStr, time.Local)
|
|
|
b83884 |
- startStr = start.Format(format)
|
|
|
b83884 |
+ var err error
|
|
|
b83884 |
+ start, err = time.ParseInLocation(format, startStr, time.Local)
|
|
|
b83884 |
+ if err != nil {
|
|
|
b83884 |
+ return "", err
|
|
|
b83884 |
+ }
|
|
|
b83884 |
}
|
|
|
b83884 |
|
|
|
b83884 |
end = start.Add(time.Minute * time.Duration(minutes))
|
|
|
b83884 |
endStr = end.Format(format)
|
|
|
b83884 |
|
|
|
b83884 |
- // create sha1 encode string
|
|
|
b83884 |
- sh := sha1.New()
|
|
|
b83884 |
- if _, err := sh.Write([]byte(data + setting.SecretKey + startStr + endStr +
|
|
|
b83884 |
- com.ToStr(minutes))); err != nil {
|
|
|
b83884 |
- return "", err
|
|
|
b83884 |
+ // create HMAC-SHA256 encoded string
|
|
|
b83884 |
+ key := []byte(setting.SecretKey)
|
|
|
b83884 |
+ h := hmac.New(sha256.New, key)
|
|
|
b83884 |
+ if _, err := h.Write([]byte(payload + startStr + endStr)); err != nil {
|
|
|
b83884 |
+ return "", fmt.Errorf("cannot create hmac: %v", err)
|
|
|
b83884 |
}
|
|
|
b83884 |
- encoded := hex.EncodeToString(sh.Sum(nil))
|
|
|
b83884 |
+ encoded := hex.EncodeToString(h.Sum(nil))
|
|
|
b83884 |
|
|
|
b83884 |
code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
|
|
|
b83884 |
return code, nil
|
|
|
05d305 |
@@ -50,29 +55,32 @@ func createTimeLimitCode(data string, minutes int, startInf interface{}) (string
|
|
|
b83884 |
|
|
|
b83884 |
// verify time limit code
|
|
|
05d305 |
func validateUserEmailCode(cfg *setting.Cfg, user *models.User, code string) (bool, error) {
|
|
|
b83884 |
- if len(code) <= 18 {
|
|
|
b83884 |
+ if len(code) < timeLimitCodeLength {
|
|
|
b83884 |
return false, nil
|
|
|
b83884 |
}
|
|
|
b83884 |
|
|
|
05d305 |
- minutes := cfg.EmailCodeValidMinutes
|
|
|
b83884 |
code = code[:timeLimitCodeLength]
|
|
|
b83884 |
|
|
|
b83884 |
// split code
|
|
|
b83884 |
- start := code[:12]
|
|
|
b83884 |
- lives := code[12:18]
|
|
|
b83884 |
- if d, err := com.StrTo(lives).Int(); err == nil {
|
|
|
b83884 |
- minutes = d
|
|
|
b83884 |
+ startStr := code[:timeLimitStartDateLength]
|
|
|
b83884 |
+ minutesStr := code[timeLimitStartDateLength : timeLimitStartDateLength+timeLimitMinutesLength]
|
|
|
b83884 |
+ minutes, err := strconv.Atoi(minutesStr)
|
|
|
b83884 |
+ if err != nil {
|
|
|
b83884 |
+ return false, fmt.Errorf("invalid time limit code: %v", err)
|
|
|
b83884 |
}
|
|
|
b83884 |
|
|
|
05d305 |
// right active code
|
|
|
b83884 |
- data := com.ToStr(user.Id) + user.Email + user.Login + user.Password + user.Rands
|
|
|
b83884 |
- retCode, err := createTimeLimitCode(data, minutes, start)
|
|
|
b83884 |
+ payload := strconv.FormatInt(user.Id, 10) + user.Email + user.Login + user.Password + user.Rands
|
|
|
b83884 |
+ expectedCode, err := createTimeLimitCode(payload, minutes, startStr)
|
|
|
b83884 |
if err != nil {
|
|
|
b83884 |
return false, err
|
|
|
b83884 |
}
|
|
|
b83884 |
- if retCode == code && minutes > 0 {
|
|
|
b83884 |
+ if hmac.Equal([]byte(code), []byte(expectedCode)) && minutes > 0 {
|
|
|
b83884 |
// check time is expired or not
|
|
|
b83884 |
- before, _ := time.ParseInLocation("200601021504", start, time.Local)
|
|
|
05d305 |
+ before, err := time.ParseInLocation("200601021504", startStr, time.Local)
|
|
|
05d305 |
+ if err != nil {
|
|
|
05d305 |
+ return false, err
|
|
|
05d305 |
+ }
|
|
|
b83884 |
now := time.Now()
|
|
|
b83884 |
if before.Add(time.Minute*time.Duration(minutes)).Unix() > now.Unix() {
|
|
|
b83884 |
return true, nil
|
|
|
05d305 |
@@ -93,15 +101,15 @@ func getLoginForEmailCode(code string) string {
|
|
|
b83884 |
return string(b)
|
|
|
b83884 |
}
|
|
|
b83884 |
|
|
|
05d305 |
-func createUserEmailCode(cfg *setting.Cfg, u *models.User, startInf interface{}) (string, error) {
|
|
|
05d305 |
+func createUserEmailCode(cfg *setting.Cfg, user *models.User, startStr string) (string, error) {
|
|
|
05d305 |
minutes := cfg.EmailCodeValidMinutes
|
|
|
b83884 |
- data := com.ToStr(u.Id) + u.Email + u.Login + u.Password + u.Rands
|
|
|
b83884 |
- code, err := createTimeLimitCode(data, minutes, startInf)
|
|
|
b83884 |
+ payload := strconv.FormatInt(user.Id, 10) + user.Email + user.Login + user.Password + user.Rands
|
|
|
b83884 |
+ code, err := createTimeLimitCode(payload, minutes, startStr)
|
|
|
b83884 |
if err != nil {
|
|
|
b83884 |
return "", err
|
|
|
b83884 |
}
|
|
|
b83884 |
|
|
|
b83884 |
// add tail hex username
|
|
|
b83884 |
- code += hex.EncodeToString([]byte(u.Login))
|
|
|
b83884 |
+ code += hex.EncodeToString([]byte(user.Login))
|
|
|
b83884 |
return code, nil
|
|
|
b83884 |
}
|
|
|
b83884 |
diff --git a/pkg/services/notifications/codes_test.go b/pkg/services/notifications/codes_test.go
|
|
|
05d305 |
index a314c8deca..be9b68ca69 100644
|
|
|
b83884 |
--- a/pkg/services/notifications/codes_test.go
|
|
|
b83884 |
+++ b/pkg/services/notifications/codes_test.go
|
|
|
05d305 |
@@ -1,7 +1,10 @@
|
|
|
b83884 |
package notifications
|
|
|
b83884 |
|
|
|
b83884 |
import (
|
|
|
b83884 |
+ "fmt"
|
|
|
b83884 |
+ "strconv"
|
|
|
b83884 |
"testing"
|
|
|
b83884 |
+ "time"
|
|
|
b83884 |
|
|
|
b83884 |
"github.com/grafana/grafana/pkg/models"
|
|
|
b83884 |
"github.com/grafana/grafana/pkg/setting"
|
|
|
05d305 |
@@ -9,18 +12,126 @@ import (
|
|
|
05d305 |
"github.com/stretchr/testify/require"
|
|
|
b83884 |
)
|
|
|
b83884 |
|
|
|
b83884 |
+func TestTimeLimitCodes(t *testing.T) {
|
|
|
05d305 |
+ cfg := setting.NewCfg()
|
|
|
05d305 |
+ cfg.EmailCodeValidMinutes = 120
|
|
|
b83884 |
+ user := &models.User{Id: 10, Email: "t@a.com", Login: "asd", Password: "1", Rands: "2"}
|
|
|
b83884 |
+
|
|
|
b83884 |
+ format := "200601021504"
|
|
|
b83884 |
+ mailPayload := strconv.FormatInt(user.Id, 10) + user.Email + user.Login + user.Password + user.Rands
|
|
|
b83884 |
+ tenMinutesAgo := time.Now().Add(-time.Minute * 10)
|
|
|
b83884 |
+
|
|
|
b83884 |
+ tests := []struct {
|
|
|
b83884 |
+ desc string
|
|
|
b83884 |
+ payload string
|
|
|
b83884 |
+ start time.Time
|
|
|
b83884 |
+ minutes int
|
|
|
b83884 |
+ valid bool
|
|
|
b83884 |
+ }{
|
|
|
b83884 |
+ {
|
|
|
b83884 |
+ desc: "code generated 10 minutes ago, 5 minutes valid",
|
|
|
b83884 |
+ payload: mailPayload,
|
|
|
b83884 |
+ start: tenMinutesAgo,
|
|
|
b83884 |
+ minutes: 5,
|
|
|
b83884 |
+ valid: false,
|
|
|
b83884 |
+ },
|
|
|
b83884 |
+ {
|
|
|
b83884 |
+ desc: "code generated 10 minutes ago, 9 minutes valid",
|
|
|
b83884 |
+ payload: mailPayload,
|
|
|
b83884 |
+ start: tenMinutesAgo,
|
|
|
b83884 |
+ minutes: 9,
|
|
|
b83884 |
+ valid: false,
|
|
|
b83884 |
+ },
|
|
|
b83884 |
+ {
|
|
|
b83884 |
+ desc: "code generated 10 minutes ago, 10 minutes valid",
|
|
|
b83884 |
+ payload: mailPayload,
|
|
|
b83884 |
+ start: tenMinutesAgo,
|
|
|
b83884 |
+ minutes: 10,
|
|
|
b83884 |
+ // code was valid exactly 10 minutes since evaluating the tenMinutesAgo assignment
|
|
|
b83884 |
+ // by the time this test is run the code is already expired
|
|
|
b83884 |
+ valid: false,
|
|
|
b83884 |
+ },
|
|
|
b83884 |
+ {
|
|
|
b83884 |
+ desc: "code generated 10 minutes ago, 11 minutes valid",
|
|
|
b83884 |
+ payload: mailPayload,
|
|
|
b83884 |
+ start: tenMinutesAgo,
|
|
|
b83884 |
+ minutes: 11,
|
|
|
b83884 |
+ valid: true,
|
|
|
b83884 |
+ },
|
|
|
b83884 |
+ {
|
|
|
b83884 |
+ desc: "code generated 10 minutes ago, 20 minutes valid",
|
|
|
b83884 |
+ payload: mailPayload,
|
|
|
b83884 |
+ start: tenMinutesAgo,
|
|
|
b83884 |
+ minutes: 20,
|
|
|
b83884 |
+ valid: true,
|
|
|
b83884 |
+ },
|
|
|
b83884 |
+ {
|
|
|
b83884 |
+ desc: "code generated 10 minutes ago, 20 minutes valid, tampered payload",
|
|
|
b83884 |
+ payload: mailPayload[:len(mailPayload)-1] + "x",
|
|
|
b83884 |
+ start: tenMinutesAgo,
|
|
|
b83884 |
+ minutes: 20,
|
|
|
b83884 |
+ valid: false,
|
|
|
b83884 |
+ },
|
|
|
b83884 |
+ }
|
|
|
b83884 |
+
|
|
|
b83884 |
+ for _, test := range tests {
|
|
|
b83884 |
+ t.Run(test.desc, func(t *testing.T) {
|
|
|
b83884 |
+ code, err := createTimeLimitCode(test.payload, test.minutes, test.start.Format(format))
|
|
|
b83884 |
+ require.NoError(t, err)
|
|
|
b83884 |
+
|
|
|
05d305 |
+ isValid, err := validateUserEmailCode(cfg, user, code)
|
|
|
b83884 |
+ require.NoError(t, err)
|
|
|
b83884 |
+ require.Equal(t, test.valid, isValid)
|
|
|
b83884 |
+ })
|
|
|
b83884 |
+ }
|
|
|
b83884 |
+
|
|
|
b83884 |
+ t.Run("tampered minutes", func(t *testing.T) {
|
|
|
b83884 |
+ code, err := createTimeLimitCode(mailPayload, 5, tenMinutesAgo.Format(format))
|
|
|
b83884 |
+ require.NoError(t, err)
|
|
|
b83884 |
+
|
|
|
b83884 |
+ // code is expired
|
|
|
05d305 |
+ isValid, err := validateUserEmailCode(cfg, user, code)
|
|
|
b83884 |
+ require.NoError(t, err)
|
|
|
b83884 |
+ require.Equal(t, false, isValid)
|
|
|
b83884 |
+
|
|
|
b83884 |
+ // let's try to extend the code by tampering the minutes
|
|
|
b83884 |
+ code = code[:12] + fmt.Sprintf("%06d", 20) + code[18:]
|
|
|
05d305 |
+ isValid, err = validateUserEmailCode(cfg, user, code)
|
|
|
b83884 |
+ require.NoError(t, err)
|
|
|
b83884 |
+ require.Equal(t, false, isValid)
|
|
|
b83884 |
+ })
|
|
|
b83884 |
+
|
|
|
b83884 |
+ t.Run("tampered start string", func(t *testing.T) {
|
|
|
b83884 |
+ code, err := createTimeLimitCode(mailPayload, 5, tenMinutesAgo.Format(format))
|
|
|
b83884 |
+ require.NoError(t, err)
|
|
|
b83884 |
+
|
|
|
b83884 |
+ // code is expired
|
|
|
05d305 |
+ isValid, err := validateUserEmailCode(cfg, user, code)
|
|
|
b83884 |
+ require.NoError(t, err)
|
|
|
b83884 |
+ require.Equal(t, false, isValid)
|
|
|
b83884 |
+
|
|
|
b83884 |
+ // let's try to extend the code by tampering the start string
|
|
|
b83884 |
+ oneMinuteAgo := time.Now().Add(-time.Minute)
|
|
|
b83884 |
+
|
|
|
b83884 |
+ code = oneMinuteAgo.Format(format) + code[12:]
|
|
|
05d305 |
+ isValid, err = validateUserEmailCode(cfg, user, code)
|
|
|
b83884 |
+ require.NoError(t, err)
|
|
|
b83884 |
+ require.Equal(t, false, isValid)
|
|
|
b83884 |
+ })
|
|
|
b83884 |
+}
|
|
|
b83884 |
+
|
|
|
b83884 |
func TestEmailCodes(t *testing.T) {
|
|
|
05d305 |
t.Run("When generating code", func(t *testing.T) {
|
|
|
05d305 |
cfg := setting.NewCfg()
|
|
|
05d305 |
cfg.EmailCodeValidMinutes = 120
|
|
|
b83884 |
|
|
|
b83884 |
user := &models.User{Id: 10, Email: "t@a.com", Login: "asd", Password: "1", Rands: "2"}
|
|
|
05d305 |
- code, err := createUserEmailCode(cfg, user, nil)
|
|
|
05d305 |
+ code, err := createUserEmailCode(cfg, user, "")
|
|
|
05d305 |
require.NoError(t, err)
|
|
|
05d305 |
|
|
|
05d305 |
t.Run("getLoginForCode should return login", func(t *testing.T) {
|
|
|
05d305 |
login := getLoginForEmailCode(code)
|
|
|
05d305 |
- require.Equal(t, login, "asd")
|
|
|
05d305 |
+ require.Equal(t, "asd", login)
|
|
|
05d305 |
})
|
|
|
b83884 |
|
|
|
05d305 |
t.Run("Can verify valid code", func(t *testing.T) {
|
|
|
05d305 |
@@ -29,7 +140,7 @@ func TestEmailCodes(t *testing.T) {
|
|
|
05d305 |
require.True(t, isValid)
|
|
|
b83884 |
})
|
|
|
b83884 |
|
|
|
05d305 |
- t.Run("Cannot verify in-valid code", func(t *testing.T) {
|
|
|
05d305 |
+ t.Run("Cannot verify invalid code", func(t *testing.T) {
|
|
|
b83884 |
code = "ASD"
|
|
|
05d305 |
isValid, err := validateUserEmailCode(cfg, user, code)
|
|
|
05d305 |
require.NoError(t, err)
|
|
|
b83884 |
diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go
|
|
|
05d305 |
index 84a0d42cb6..52facd0992 100644
|
|
|
b83884 |
--- a/pkg/services/notifications/notifications.go
|
|
|
b83884 |
+++ b/pkg/services/notifications/notifications.go
|
|
|
05d305 |
@@ -168,7 +168,7 @@ func (ns *NotificationService) SendEmailCommandHandler(ctx context.Context, cmd
|
|
|
b83884 |
}
|
|
|
b83884 |
|
|
|
05d305 |
func (ns *NotificationService) SendResetPasswordEmail(ctx context.Context, cmd *models.SendResetPasswordEmailCommand) error {
|
|
|
05d305 |
- code, err := createUserEmailCode(ns.Cfg, cmd.User, nil)
|
|
|
05d305 |
+ code, err := createUserEmailCode(ns.Cfg, cmd.User, "")
|
|
|
b83884 |
if err != nil {
|
|
|
b83884 |
return err
|
|
|
b83884 |
}
|
|
|
b83884 |
diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go
|
|
|
05d305 |
index 71970e20a0..6f4b318fe0 100644
|
|
|
b83884 |
--- a/pkg/services/notifications/notifications_test.go
|
|
|
b83884 |
+++ b/pkg/services/notifications/notifications_test.go
|
|
|
05d305 |
@@ -2,6 +2,7 @@ package notifications
|
|
|
b83884 |
|
|
|
b83884 |
import (
|
|
|
05d305 |
"context"
|
|
|
b83884 |
+ "regexp"
|
|
|
b83884 |
"testing"
|
|
|
b83884 |
|
|
|
b83884 |
"github.com/grafana/grafana/pkg/bus"
|
|
|
05d305 |
@@ -185,7 +186,8 @@ func TestSendEmailAsync(t *testing.T) {
|
|
|
b83884 |
|
|
|
05d305 |
t.Run("When sending reset email password", func(t *testing.T) {
|
|
|
05d305 |
sut, _ := createSut(t, bus)
|
|
|
05d305 |
- err := sut.SendResetPasswordEmail(context.Background(), &models.SendResetPasswordEmailCommand{User: &models.User{Email: "asd@asd.com"}})
|
|
|
05d305 |
+ user := models.User{Email: "asd@asd.com", Login: "asd@asd.com"}
|
|
|
05d305 |
+ err := sut.SendResetPasswordEmail(context.Background(), &models.SendResetPasswordEmailCommand{User: &user})
|
|
|
05d305 |
require.NoError(t, err)
|
|
|
b83884 |
|
|
|
05d305 |
sentMsg := <-sut.mailQueue
|
|
|
05d305 |
@@ -194,6 +196,21 @@ func TestSendEmailAsync(t *testing.T) {
|
|
|
05d305 |
assert.Equal(t, "Reset your Grafana password - asd@asd.com", sentMsg.Subject)
|
|
|
05d305 |
assert.NotContains(t, sentMsg.Body["text/html"], "Subject")
|
|
|
05d305 |
assert.NotContains(t, sentMsg.Body["text/plain"], "Subject")
|
|
|
b83884 |
+
|
|
|
05d305 |
+ // find code in mail
|
|
|
05d305 |
+ r, _ := regexp.Compile(`code=(\w+)`)
|
|
|
05d305 |
+ match := r.FindString(sentMsg.Body["text/plain"])
|
|
|
05d305 |
+ code := match[len("code="):]
|
|
|
b83884 |
+
|
|
|
05d305 |
+ // verify code
|
|
|
05d305 |
+ query := models.ValidateResetPasswordCodeQuery{Code: code}
|
|
|
05d305 |
+ getUserByLogin := func(ctx context.Context, login string) (*models.User, error) {
|
|
|
05d305 |
+ query := models.GetUserByLoginQuery{LoginOrEmail: login}
|
|
|
05d305 |
+ query.Result = &user
|
|
|
05d305 |
+ return query.Result, nil
|
|
|
05d305 |
+ }
|
|
|
05d305 |
+ err = sut.ValidateResetPasswordCode(context.Background(), &query, getUserByLogin)
|
|
|
05d305 |
+ require.NoError(t, err)
|
|
|
b83884 |
})
|
|
|
05d305 |
|
|
|
05d305 |
t.Run("When SMTP disabled in configuration", func(t *testing.T) {
|