You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/libs/crypto/crypto.go

137 lines
3.2 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-03-04 16:08:06
// Distributed under terms of the MIT license.
//
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"golang.org/x/crypto/bcrypt"
)
// HashPassword 使用bcrypt哈希密码
func HashPassword(password string, cost int) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
if err != nil {
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(bytes), nil
}
// VerifyPassword 验证密码
func VerifyPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// GenerateRandomString 生成随机字符串
func GenerateRandomString(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes)[:length], nil
}
// GenerateSecret 生成密钥
func GenerateSecret(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
bytes := make([]byte, length)
rand.Read(bytes)
for i, b := range bytes {
bytes[i] = charset[b%byte(len(charset))]
}
return string(bytes)
}
// GenerateClientID 生成OAuth客户端ID
func GenerateClientID() string {
return "vc_" + GenerateSecret(28)
}
// GenerateClientSecret 生成OAuth客户端密钥
func GenerateClientSecret() string {
return GenerateSecret(64)
}
// deriveKey 从密钥字符串派生32字节AES密钥
func deriveKey(key string) []byte {
hash := sha256.Sum256([]byte(key))
return hash[:]
}
// Encrypt 使用AES-GCM加密数据
// key: 加密密钥会被派生为32字节
// plaintext: 明文数据
// 返回: base64编码的密文包含nonce
func Encrypt(key, plaintext string) (string, error) {
if key == "" || plaintext == "" {
return "", fmt.Errorf("key and plaintext cannot be empty")
}
block, err := aes.NewCipher(deriveKey(key))
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt 使用AES-GCM解密数据
// key: 解密密钥
// ciphertext: base64编码的密文包含nonce
// 返回: 明文数据
func Decrypt(key, ciphertext string) (string, error) {
if key == "" || ciphertext == "" {
return "", fmt.Errorf("key and ciphertext cannot be empty")
}
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", fmt.Errorf("failed to decode ciphertext: %w", err)
}
block, err := aes.NewCipher(deriveKey(key))
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return "", fmt.Errorf("ciphertext too short")
}
nonce, encrypted := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, encrypted, nil)
if err != nil {
return "", fmt.Errorf("failed to decrypt: %w", err)
}
return string(plaintext), nil
}