init
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const passwordRounds = 150000
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
func RandomSecret() string {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
func CheckPassword(encoded, password string) bool {
|
||||
if strings.HasPrefix(encoded, "$2") {
|
||||
return bcrypt.CompareHashAndPassword([]byte(encoded), []byte(password)) == nil
|
||||
}
|
||||
parts := strings.Split(encoded, "$")
|
||||
if len(parts) != 4 || parts[0] != "sha256" {
|
||||
return false
|
||||
}
|
||||
rounds, err := strconv.Atoi(parts[1])
|
||||
if err != nil || rounds < 10000 || rounds > 1000000 {
|
||||
return false
|
||||
}
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[2])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
want, err := base64.RawStdEncoding.DecodeString(parts[3])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
got := stretch([]byte(password), salt, rounds)
|
||||
return len(got) == len(want) && subtle.ConstantTimeCompare(got, want) == 1
|
||||
}
|
||||
|
||||
func stretch(password, salt []byte, rounds int) []byte {
|
||||
buf := append(append([]byte{}, salt...), password...)
|
||||
s := sha256.Sum256(buf)
|
||||
out := s[:]
|
||||
for i := 1; i < rounds; i++ {
|
||||
h := sha256.New()
|
||||
h.Write(out)
|
||||
h.Write(salt)
|
||||
h.Write(password)
|
||||
out = h.Sum(nil)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func SignSession(secret, username string, expires time.Time) string {
|
||||
payload := fmt.Sprintf("%s|%d", username, expires.Unix())
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(payload))
|
||||
sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." + sig
|
||||
}
|
||||
|
||||
func VerifySession(secret, token string, now time.Time) (string, bool) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 2 {
|
||||
return "", false
|
||||
}
|
||||
payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write(payloadBytes)
|
||||
want := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
if subtle.ConstantTimeCompare([]byte(want), []byte(parts[1])) != 1 {
|
||||
return "", false
|
||||
}
|
||||
fields := strings.Split(string(payloadBytes), "|")
|
||||
if len(fields) != 2 {
|
||||
return "", false
|
||||
}
|
||||
exp, err := strconv.ParseInt(fields[1], 10, 64)
|
||||
if err != nil || now.Unix() > exp {
|
||||
return "", false
|
||||
}
|
||||
return fields[0], true
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPassword(t *testing.T) {
|
||||
h, e := HashPassword("secret")
|
||||
if e != nil || !CheckPassword(h, "secret") || CheckPassword(h, "wrong") {
|
||||
t.Fatal("password hash check failed")
|
||||
}
|
||||
if !strings.HasPrefix(h, "$2") {
|
||||
t.Fatal("new hashes must use bcrypt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyPasswordCompatibility(t *testing.T) {
|
||||
salt := []byte("sixteen-byte-salt")
|
||||
password := "legacy password"
|
||||
hash := stretch([]byte(password), salt, passwordRounds)
|
||||
encoded := fmt.Sprintf("sha256$%d$%s$%s", passwordRounds, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(hash))
|
||||
if !CheckPassword(encoded, password) || CheckPassword(encoded, "wrong") {
|
||||
t.Fatal("legacy hash compatibility")
|
||||
}
|
||||
if CheckPassword(strings.Replace(encoded, "150000", "999999999", 1), password) {
|
||||
t.Fatal("unbounded legacy rounds")
|
||||
}
|
||||
}
|
||||
func TestSession(t *testing.T) {
|
||||
tok := SignSession("s", "admin", time.Now().Add(time.Hour))
|
||||
u, ok := VerifySession("s", tok, time.Now())
|
||||
if !ok || u != "admin" {
|
||||
t.Fatal("session verify failed")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user