This commit is contained in:
mangomqy
2025-11-13 02:54:06 +00:00
commit c5e51ed069
254 changed files with 54901 additions and 0 deletions

View File

@ -0,0 +1,128 @@
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"io"
)
// Encryptor 加密器接口
type Encryptor interface {
Encrypt(plaintext string) (string, error)
Decrypt(ciphertext string) (string, error)
}
// AESEncryptor AES 加密器
type AESEncryptor struct {
key []byte
}
// NewAESEncryptor 创建 AES 加密器
// key: 加密密钥会自动派生为32字节密钥
func NewAESEncryptor(key string) *AESEncryptor {
// 使用 SHA256 派生固定长度的密钥
hash := sha256.Sum256([]byte(key))
return &AESEncryptor{
key: hash[:],
}
}
// Encrypt 加密字符串
// 返回 Base64 编码的密文
func (e *AESEncryptor) Encrypt(plaintext string) (string, error) {
if plaintext == "" {
return "", nil
}
block, err := aes.NewCipher(e.key)
if err != nil {
return "", err
}
// 创建 GCM 模式
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
// 生成随机 nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
// 加密nonce 会自动附加到密文前面)
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
// Base64 编码
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt 解密字符串
// 输入为 Base64 编码的密文
func (e *AESEncryptor) Decrypt(ciphertext string) (string, error) {
if ciphertext == "" {
return "", nil
}
// Base64 解码
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
block, err := aes.NewCipher(e.key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return "", errors.New("ciphertext too short")
}
// 提取 nonce 和实际密文
nonce, cipherBytes := data[:nonceSize], data[nonceSize:]
// 解密
plaintext, err := gcm.Open(nil, nonce, cipherBytes, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
// MaskSensitiveData 脱敏显示敏感数据
// 如果数据为空或已加密标记,返回掩码
func MaskSensitiveData(data string) string {
if data == "" {
return ""
}
return "••••••••" // 统一返回8个点不泄露长度信息
}
// IsEncrypted 检查字符串是否已加密
// 简单检查:加密后的数据是 Base64 格式且长度较长
func IsEncrypted(data string) bool {
if data == "" {
return false
}
// 加密后的数据至少有 nonce(12) + tag(16) + 内容Base64后会更长
if len(data) < 40 {
return false
}
// 尝试 Base64 解码
_, err := base64.StdEncoding.DecodeString(data)
return err == nil
}

View File

@ -0,0 +1,124 @@
package crypto
import (
"testing"
)
func TestAESEncryptor(t *testing.T) {
encryptor := NewAESEncryptor("test-secret-key")
tests := []struct {
name string
plaintext string
}{
{"simple password", "password123"},
{"harbor password", "BWGDIP@ssw0rd1401#"},
{"empty string", ""},
{"long certificate", "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pP"},
{"unicode", "密码123!@#"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 测试加密
encrypted, err := encryptor.Encrypt(tt.plaintext)
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
// 空字符串应该返回空
if tt.plaintext == "" {
if encrypted != "" {
t.Errorf("Expected empty encrypted string, got %s", encrypted)
}
return
}
// 加密后应该不同
if encrypted == tt.plaintext {
t.Errorf("Encrypted text should differ from plaintext")
}
// 测试解密
decrypted, err := encryptor.Decrypt(encrypted)
if err != nil {
t.Fatalf("Decrypt failed: %v", err)
}
// 解密后应该相同
if decrypted != tt.plaintext {
t.Errorf("Decrypted text mismatch: got %s, want %s", decrypted, tt.plaintext)
}
})
}
}
func TestMaskSensitiveData(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"normal password", "password123", "••••••••"},
{"empty string", "", ""},
{"long string", "very-long-password-with-many-characters", "••••••••"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := MaskSensitiveData(tt.input)
if result != tt.expected {
t.Errorf("MaskSensitiveData(%s) = %s, want %s", tt.input, result, tt.expected)
}
})
}
}
func TestIsEncrypted(t *testing.T) {
encryptor := NewAESEncryptor("test-key")
plaintext := "password123"
encrypted, _ := encryptor.Encrypt(plaintext)
tests := []struct {
name string
input string
expected bool
}{
{"encrypted data", encrypted, true},
{"plaintext", "password123", false},
{"empty string", "", false},
{"short string", "abc", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsEncrypted(tt.input)
if result != tt.expected {
t.Errorf("IsEncrypted(%s) = %v, want %v", tt.input, result, tt.expected)
}
})
}
}
func TestEncryptionConsistency(t *testing.T) {
encryptor := NewAESEncryptor("consistent-key")
plaintext := "test-password"
// 多次加密同一内容,结果应该不同(因为使用随机 nonce
encrypted1, _ := encryptor.Encrypt(plaintext)
encrypted2, _ := encryptor.Encrypt(plaintext)
if encrypted1 == encrypted2 {
t.Error("Multiple encryptions of same plaintext should produce different ciphertexts")
}
// 但解密结果应该相同
decrypted1, _ := encryptor.Decrypt(encrypted1)
decrypted2, _ := encryptor.Decrypt(encrypted2)
if decrypted1 != plaintext || decrypted2 != plaintext {
t.Error("Decryption should produce original plaintext")
}
}

View File

@ -0,0 +1,123 @@
package jwt
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
const (
AccessTokenDuration = 24 * time.Hour // Access Token 有效期
RefreshTokenDuration = 7 * 24 * time.Hour // Refresh Token 有效期
)
// JWTManager JWT 管理器
type JWTManager struct {
secretKey string
}
// NewJWTManager 创建 JWT 管理器
func NewJWTManager(secretKey string) *JWTManager {
return &JWTManager{
secretKey: secretKey,
}
}
// Claims JWT Claims
type Claims struct {
UserID string `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
// Generate 生成 Access Token 和 Refresh Token
func (m *JWTManager) Generate(userID, username string) (accessToken, refreshToken string, err error) {
// 生成 Access Token
accessClaims := &Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessToken, err = accessTokenObj.SignedString([]byte(m.secretKey))
if err != nil {
return "", "", fmt.Errorf("failed to sign access token: %w", err)
}
// 生成 Refresh Token
refreshClaims := &Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(RefreshTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshToken, err = refreshTokenObj.SignedString([]byte(m.secretKey))
if err != nil {
return "", "", fmt.Errorf("failed to sign refresh token: %w", err)
}
return accessToken, refreshToken, nil
}
// Verify 验证 Token
func (m *JWTManager) Verify(tokenString string) (userID, username string, err error) {
userID, username, _, err = m.VerifyWithIssuedAt(tokenString)
return userID, username, err
}
// VerifyWithIssuedAt 验证 Token 并返回签发时间
func (m *JWTManager) VerifyWithIssuedAt(tokenString string) (userID, username string, issuedAt int64, err error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(m.secretKey), nil
})
if err != nil {
return "", "", 0, fmt.Errorf("failed to parse token: %w", err)
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims.UserID, claims.Username, claims.IssuedAt.Unix(), nil
}
return "", "", 0, fmt.Errorf("invalid token")
}
// Refresh 刷新 Token
func (m *JWTManager) Refresh(refreshToken string) (string, error) {
// 验证 Refresh Token
userID, username, err := m.Verify(refreshToken)
if err != nil {
return "", fmt.Errorf("invalid refresh token: %w", err)
}
// 生成新的 Access Token
accessClaims := &Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
newAccessToken, err := accessTokenObj.SignedString([]byte(m.secretKey))
if err != nil {
return "", fmt.Errorf("failed to sign new access token: %w", err)
}
return newAccessToken, nil
}

View File

@ -0,0 +1,97 @@
package password
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
const (
// Argon2id 参数
memory = 64 * 1024 // 64 MB
iterations = 3
parallelism = 2
saltLength = 16
keyLength = 32
)
// Hasher 密码哈希器
type Hasher struct{}
// NewHasher 创建密码哈希器
func NewHasher() *Hasher {
return &Hasher{}
}
// Hash 哈希密码(使用 Argon2id
func (h *Hasher) Hash(password string) (string, error) {
// 生成随机 salt
salt := make([]byte, saltLength)
if _, err := rand.Read(salt); err != nil {
return "", fmt.Errorf("failed to generate salt: %w", err)
}
// 使用 Argon2id 哈希密码
hash := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, keyLength)
// 编码为字符串格式: $argon2id$v=19$m=65536,t=3,p=2$salt$hash
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version, memory, iterations, parallelism, b64Salt, b64Hash)
return encodedHash, nil
}
// Verify 验证密码
func (h *Hasher) Verify(password, encodedHash string) error {
// 解析编码的哈希
parts := strings.Split(encodedHash, "$")
if len(parts) != 6 {
return fmt.Errorf("invalid hash format")
}
if parts[1] != "argon2id" {
return fmt.Errorf("unsupported algorithm: %s", parts[1])
}
// 解析参数
var version int
var m, t, p uint32
_, err := fmt.Sscanf(parts[2], "v=%d", &version)
if err != nil {
return fmt.Errorf("failed to parse version: %w", err)
}
_, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p)
if err != nil {
return fmt.Errorf("failed to parse parameters: %w", err)
}
// 解码 salt 和 hash
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return fmt.Errorf("failed to decode salt: %w", err)
}
hash, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return fmt.Errorf("failed to decode hash: %w", err)
}
// 使用相同参数哈希输入的密码
computedHash := argon2.IDKey([]byte(password), salt, t, m, uint8(p), uint32(len(hash)))
// 使用常量时间比较防止时序攻击
if subtle.ConstantTimeCompare(hash, computedHash) == 1 {
return nil
}
return fmt.Errorf("password does not match")
}