This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const creatorPasswordIterations = 120000
|
||||
|
||||
var ErrInvalidCreatorPassword = errors.New("invalid creator password")
|
||||
|
||||
func HashCreatorPassword(password string) (string, error) {
|
||||
if len(password) < 6 || len(password) > 4096 {
|
||||
return "", ErrInvalidCreatorPassword
|
||||
}
|
||||
salt := make([]byte, 16)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
derived := pbkdf2SHA256([]byte(password), salt, creatorPasswordIterations, 32)
|
||||
return "pbkdf2-sha256$" + strconv.Itoa(creatorPasswordIterations) + "$" + base64.RawStdEncoding.EncodeToString(salt) + "$" + base64.RawStdEncoding.EncodeToString(derived), nil
|
||||
}
|
||||
|
||||
func VerifyCreatorPassword(encoded, password string) bool {
|
||||
parts := strings.Split(encoded, "$")
|
||||
if len(parts) != 4 || parts[0] != "pbkdf2-sha256" || password == "" {
|
||||
return false
|
||||
}
|
||||
iterations, err := strconv.Atoi(parts[1])
|
||||
if err != nil || iterations < 100000 || iterations > 1000000 {
|
||||
return false
|
||||
}
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[2])
|
||||
if err != nil || len(salt) < 16 {
|
||||
return false
|
||||
}
|
||||
expected, err := base64.RawStdEncoding.DecodeString(parts[3])
|
||||
if err != nil || len(expected) != 32 {
|
||||
return false
|
||||
}
|
||||
actual := pbkdf2SHA256([]byte(password), salt, iterations, len(expected))
|
||||
return subtle.ConstantTimeCompare(actual, expected) == 1
|
||||
}
|
||||
|
||||
func pbkdf2SHA256(password, salt []byte, iterations, keyLength int) []byte {
|
||||
key := make([]byte, 0, keyLength)
|
||||
for block := 1; len(key) < keyLength; block++ {
|
||||
mac := hmac.New(sha256.New, password)
|
||||
mac.Write(salt)
|
||||
mac.Write([]byte{byte(block >> 24), byte(block >> 16), byte(block >> 8), byte(block)})
|
||||
u := mac.Sum(nil)
|
||||
t := append([]byte(nil), u...)
|
||||
for i := 1; i < iterations; i++ {
|
||||
mac = hmac.New(sha256.New, password)
|
||||
mac.Write(u)
|
||||
u = mac.Sum(nil)
|
||||
for j := range t {
|
||||
t[j] ^= u[j]
|
||||
}
|
||||
}
|
||||
key = append(key, t...)
|
||||
}
|
||||
return key[:keyLength]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package domain
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCreatorPasswordHashRoundTrip(t *testing.T) {
|
||||
hash, err := HashCreatorPassword("a-long-enough-test-password")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !VerifyCreatorPassword(hash, "a-long-enough-test-password") {
|
||||
t.Fatal("hashed password was not accepted")
|
||||
}
|
||||
if VerifyCreatorPassword(hash, "wrong-password") {
|
||||
t.Fatal("wrong password was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatorPasswordHashUsesUniqueSalt(t *testing.T) {
|
||||
first, err := HashCreatorPassword("a-long-enough-test-password")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := HashCreatorPassword("a-long-enough-test-password")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first == second {
|
||||
t.Fatal("password hashes reused a salt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatorPasswordHashRejectsShortPassword(t *testing.T) {
|
||||
if _, err := HashCreatorPassword("short"); err == nil {
|
||||
t.Fatal("short password was accepted")
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,14 @@ import (
|
||||
var ErrInvalidCreatorSession = errors.New("invalid creator session")
|
||||
|
||||
func IssueCreatorSession(secret []byte, now time.Time, ttl time.Duration) (string, identity.CreatorSession, error) {
|
||||
if len(secret) < 32 || ttl <= 0 {
|
||||
return IssueCreatorSessionForBinding(secret, identity.ActorID("domovoy-creator-actor"), identity.PrincipalID("domovoy-creator-principal"), now, ttl)
|
||||
}
|
||||
|
||||
func IssueCreatorSessionForBinding(secret []byte, actorID identity.ActorID, principalID identity.PrincipalID, now time.Time, ttl time.Duration) (string, identity.CreatorSession, error) {
|
||||
if len(secret) < 32 || actorID == "" || principalID == "" || ttl <= 0 {
|
||||
return "", identity.CreatorSession{}, ErrInvalidCreatorSession
|
||||
}
|
||||
session := identity.CreatorSession{ActorID: "domovoy-creator-actor", PrincipalID: "domovoy-creator-principal", ExpiresAtUnix: now.Add(ttl).Unix()}
|
||||
session := identity.CreatorSession{ActorID: actorID, PrincipalID: principalID, ExpiresAtUnix: now.Add(ttl).Unix()}
|
||||
payload, err := json.Marshal(session)
|
||||
if err != nil {
|
||||
return "", identity.CreatorSession{}, err
|
||||
@@ -38,7 +42,7 @@ func ValidateCreatorSession(secret []byte, token string, now time.Time) (identit
|
||||
return identity.CreatorSession{}, ErrInvalidCreatorSession
|
||||
}
|
||||
var session identity.CreatorSession
|
||||
if json.Unmarshal(payload, &session) != nil || session.ActorID != "domovoy-creator-actor" || session.PrincipalID != "domovoy-creator-principal" || now.Unix() >= session.ExpiresAtUnix {
|
||||
if json.Unmarshal(payload, &session) != nil || session.ActorID == "" || session.PrincipalID == "" || now.Unix() >= session.ExpiresAtUnix {
|
||||
return identity.CreatorSession{}, ErrInvalidCreatorSession
|
||||
}
|
||||
return session, nil
|
||||
|
||||
Reference in New Issue
Block a user