package main 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$$ 的 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 }