forked from alterminal/alterminal
first commit
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
// Role 為使用者角色:admin 具管理權限,user 為一般權限。
|
||||
type Role string
|
||||
|
||||
// 允許的角色值。
|
||||
const (
|
||||
RoleAdmin Role = "admin"
|
||||
RoleUser Role = "user"
|
||||
)
|
||||
|
||||
// Valid 回傳角色是否為允許的值。
|
||||
func (r Role) Valid() bool {
|
||||
return r == RoleAdmin || r == RoleUser
|
||||
}
|
||||
|
||||
// User 為使用者帳號模型,對應 users 資料表。
|
||||
// Username 與 Email 皆為唯一;密碼以 argon2id(PHC 格式)雜湊儲存,永不存明文。
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Username string `gorm:"uniqueIndex;size:64;not null"` // 登入帳號
|
||||
Email string `gorm:"uniqueIndex;size:255;not null"` // OIDC email scope
|
||||
EmailVerified bool `gorm:"not null;default:false"` // OIDC email_verified claim
|
||||
PasswordHash string `gorm:"size:255;not null"` // argon2id PHC 字串
|
||||
Name string `gorm:"size:255"` // 顯示名稱(profile scope 的 name claim)
|
||||
Role Role `gorm:"size:16;not null;default:user"` // admin 或 user
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// SetPassword 以 argon2id 雜湊密碼並寫入 PasswordHash。
|
||||
func (u *User) SetPassword(password string) error {
|
||||
hash, err := HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.PasswordHash = hash
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPassword 回傳密碼是否與 PasswordHash 相符;雜湊格式無效時一律視為不相符。
|
||||
func (u *User) CheckPassword(password string) bool {
|
||||
ok, err := VerifyPassword(password, u.PasswordHash)
|
||||
return err == nil && ok
|
||||
}
|
||||
|
||||
// 參數採 OWASP 對 Argon2id 的建議:m=19 MiB、t=2、p=1,salt 16 bytes、key 32 bytes。
|
||||
const (
|
||||
argon2MemoryKB = 19 * 1024
|
||||
argon2Time = 2
|
||||
argon2Threads = 1
|
||||
argon2SaltLen = 16
|
||||
argon2KeyLen = 32
|
||||
)
|
||||
|
||||
// HashPassword 產生格式如 $argon2id$v=19$m=19456,t=2,p=1$<salt>$<key> 的 PHC 字串。
|
||||
func HashPassword(password string) (string, error) {
|
||||
salt := make([]byte, argon2SaltLen)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", fmt.Errorf("read salt: %w", err)
|
||||
}
|
||||
key := argon2.IDKey([]byte(password), salt, argon2Time, argon2MemoryKB, argon2Threads, argon2KeyLen)
|
||||
return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
argon2.Version, argon2MemoryKB, argon2Time, argon2Threads,
|
||||
base64.RawStdEncoding.EncodeToString(salt),
|
||||
base64.RawStdEncoding.EncodeToString(key),
|
||||
), nil
|
||||
}
|
||||
|
||||
// VerifyPassword 解析 PHC 字串並以 constant-time 比對重算結果。
|
||||
func VerifyPassword(password, encoded string) (bool, error) {
|
||||
parts := strings.Split(encoded, "$")
|
||||
if len(parts) != 6 || parts[1] != "argon2id" {
|
||||
return false, errors.New("malformed password hash")
|
||||
}
|
||||
var version int
|
||||
if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil {
|
||||
return false, fmt.Errorf("parse version: %w", err)
|
||||
}
|
||||
if version != argon2.Version {
|
||||
return false, fmt.Errorf("unsupported argon2id version %d", version)
|
||||
}
|
||||
var memoryKB, timeCost uint32
|
||||
var threads uint8
|
||||
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memoryKB, &timeCost, &threads); err != nil {
|
||||
return false, fmt.Errorf("parse parameters: %w", err)
|
||||
}
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("decode salt: %w", err)
|
||||
}
|
||||
want, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("decode key: %w", err)
|
||||
}
|
||||
got := argon2.IDKey([]byte(password), salt, timeCost, memoryKB, threads, uint32(len(want)))
|
||||
return subtle.ConstantTimeCompare(got, want) == 1, nil
|
||||
}
|
||||
Reference in New Issue
Block a user