重組前檢查點:根目錄 main package

This commit is contained in:
2026-10-03 09:23:38 +08:00
commit f373cb8d37
45 changed files with 5332 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
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$<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
}