Files
2026-09-30 15:49:12 +08:00

1145 lines
36 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package encryptx
import (
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"os"
"strings"
"code.yun.ink/pkg/encryptx/aesx"
"code.yun.ink/pkg/encryptx/argon2x"
"code.yun.ink/pkg/encryptx/base64x"
"code.yun.ink/pkg/encryptx/bcryptx"
"code.yun.ink/pkg/encryptx/blowfishx"
"code.yun.ink/pkg/encryptx/chachax"
"code.yun.ink/pkg/encryptx/cipherx"
"code.yun.ink/pkg/encryptx/desx"
"code.yun.ink/pkg/encryptx/ecdsax"
"code.yun.ink/pkg/encryptx/ed25519x"
"code.yun.ink/pkg/encryptx/hexx"
"code.yun.ink/pkg/encryptx/hkdfx"
"code.yun.ink/pkg/encryptx/hmacx"
"code.yun.ink/pkg/encryptx/pbkdf2x"
"code.yun.ink/pkg/encryptx/rsax"
"code.yun.ink/pkg/encryptx/scryptx"
"code.yun.ink/pkg/encryptx/tdesx"
"code.yun.ink/pkg/encryptx/utilsx"
)
// 选项键名。集中定义以免拼写错误,也便于在文档中统一说明。
const (
optKeyLength = "keyLength" // 对称/HMAC 密钥长度(字节)
optSalt = "salt" // KDF 盐值(十六进制字符串或 []byte)
optSaltLength = "saltLength" // 随机盐值长度(字节)
optIterations = "iterations" // PBKDF2 迭代次数
optInfo = "info" // HKDF 上下文信息
optScryptN = "n" // scrypt CPU/内存代价
optScryptR = "r" // scrypt 块大小
optScryptP = "p" // scrypt 并行度
optArgon2Time = "time" // Argon2 迭代次数
optArgon2Memory = "memory" // Argon2 内存开销(KiB)
optArgon2Threads = "threads" // Argon2 并行度
optCost = "cost" // bcrypt 代价
optIV = "iv" // 显式 IV(十六进制),用于兼容固定 IV 的旧系统
optAAD = "aad" // AEAD 附加认证数据
optBits = "bits" // RSA 密钥位数
optPublicKeyFile = "publicKeyFile" // 公钥文件路径
optPrivateKeyFile = "privateKeyFile" // 私钥文件路径
)
// aeadSeparator 分隔 AEAD 的密文与 nonce。十六进制字符集不含冒号,因此不会冲突。
const aeadSeparator = ":"
// Encrypt 使用指定算法处理数据并返回编码后的结果。
//
// 参数:
// - encryptType:算法,必须是本包支持的取值;
// - data:待处理的数据(哈希/加密的明文,或编码的原文);
// - key:密钥。对称算法为**原始密钥字节**;RSA 为 PEM 编码的公钥;
// 编码类算法可传 nil;
// - options:算法参数,可传 nil。
//
// 返回值的编码方式按算法族不同,详见包文档。
//
// 与历史版本的差别:不再接受任何字符串形式的密钥,也不再对无法识别的选项
// 静默取默认值——键名拼错或类型不符都会返回错误。此外,给不使用密钥的算法
// (编码、哈希、口令哈希)传入 key 会返回 ErrUnexpectedKey,而不是像历史版本
// 那样静默忽略,让人误以为数据受到了密钥保护。
func Encrypt(encryptType EncryptType, data string, key []byte, options Options) (string, error) {
if !encryptType.Valid() {
return "", fmt.Errorf("%w: %d", ErrUnsupportedType, int(encryptType))
}
if options == nil {
options = Options{}
}
if err := checkKeyUsage(encryptType, key, options); err != nil {
return "", err
}
switch encryptType {
// ---- 哈希 ----
case MD5, SHA1, SHA224, SHA256, SHA384, SHA512,
SHA3_224, SHA3_256, SHA3_384, SHA3_512,
BLAKE2B_256, BLAKE2S_256, RIPEMD160:
if err := options.rejectUnknown(); err != nil {
return "", err
}
return hashHex(encryptType, []byte(data))
// ---- 消息认证码 ----
case HMAC_SHA1, HMAC_SHA256, HMAC_SHA384, HMAC_SHA512:
if err := options.rejectUnknown(); err != nil {
return "", err
}
return hmacHex(encryptType, data, key), nil
// ---- 编码 ----
case BASE64:
if err := options.rejectUnknown(); err != nil {
return "", err
}
return base64x.Encode([]byte(data)), nil
case BASE64_URL:
if err := options.rejectUnknown(); err != nil {
return "", err
}
return base64x.EncodeURL([]byte(data)), nil
case HEX:
if err := options.rejectUnknown(); err != nil {
return "", err
}
return hexx.Encode([]byte(data)), nil
// ---- 口令哈希:单向处理,因此也走 Encrypt ----
case BCRYPT:
if err := options.rejectUnknown(optCost); err != nil {
return "", err
}
return bcryptHash(data, options)
case ARGON2ID:
if err := options.rejectUnknown(optArgon2Time, optArgon2Memory, optArgon2Threads, optKeyLength); err != nil {
return "", err
}
return argon2Hash(data, options)
// ---- 密钥派生:必须显式给出盐值才能复现 ----
case PBKDF2, HKDF, SCRYPT:
return deriveHex(encryptType, data, options)
// ---- AEAD ----
case AES_GCM:
if err := options.rejectUnknown(optAAD); err != nil {
return "", err
}
aad, err := optionalStringOption(encryptType, options, optAAD)
if err != nil {
return "", err
}
ciphertext, nonce, err := aesx.EncryptGCM([]byte(data), key, aad)
if err != nil {
return "", err
}
return hexx.Encode(ciphertext) + aeadSeparator + hexx.Encode(nonce), nil
case CHACHA20_POLY1305, XCHACHA20_POLY1305:
if err := options.rejectUnknown(optAAD); err != nil {
return "", err
}
aad, err := optionalStringOption(encryptType, options, optAAD)
if err != nil {
return "", err
}
var (
ciphertext, nonce []byte
encErr error
)
if encryptType == CHACHA20_POLY1305 {
ciphertext, nonce, encErr = chachax.Encrypt(key, []byte(data), aad)
} else {
ciphertext, nonce, encErr = chachax.EncryptX(key, []byte(data), aad)
}
if encErr != nil {
return "", encErr
}
return hexx.Encode(ciphertext) + aeadSeparator + hexx.Encode(nonce), nil
// ---- 分组密码(含 ECB 这类历史模式)----
case DES_ECB, DES_CBC, DES_CFB, DES_OFB, DES_CTR,
TDES_ECB, TDES_CBC, TDES_CFB, TDES_OFB, TDES_CTR,
AES_ECB, AES_CBC, AES_CFB, AES_OFB, AES_CTR,
BLOWFISH_ECB, BLOWFISH_CBC, BLOWFISH_CFB, BLOWFISH_OFB, BLOWFISH_CTR:
if err := options.rejectUnknown(optIV); err != nil {
return "", err
}
suite, mode, err := suiteAndMode(encryptType)
if err != nil {
return "", err
}
iv, hasIV, err := optionalBytesValue(encryptType, options, optIV)
if err != nil {
return "", err
}
if hasIV {
return suite.EncryptWithIVHex(mode, []byte(data), key, iv)
}
return suite.EncryptHex(mode, []byte(data), key)
// ---- 非对称加密 ----
case RSA, RSA_OAEP:
if err := options.rejectUnknown(optPublicKeyFile); err != nil {
return "", err
}
pub, err := resolvePublicKey(key, options)
if err != nil {
return "", err
}
ciphertext, err := rsax.EncryptOAEPLong(pub, []byte(data))
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
return "", fmt.Errorf("%w: %d", ErrUnsupportedType, int(encryptType))
}
// Decrypt 撤销 Encrypt 的处理,返回原始数据。
//
// 只有可逆算法可以调用本函数;哈希、HMAC、密钥派生与口令哈希都会返回包装了
// ErrIrreversible 的错误。
func Decrypt(encryptType EncryptType, data string, key []byte, options Options) (string, error) {
if !encryptType.Valid() {
return "", fmt.Errorf("%w: %d", ErrUnsupportedType, int(encryptType))
}
if !encryptType.IsReversible() {
return "", fmt.Errorf("%w: %s", ErrIrreversible, encryptType)
}
if options == nil {
options = Options{}
}
if err := checkKeyUsage(encryptType, key, options); err != nil {
return "", err
}
switch encryptType {
case BASE64:
if err := options.rejectUnknown(); err != nil {
return "", err
}
out, err := base64x.Decode(data)
if err != nil {
return "", fmt.Errorf("%w: %w", ErrInvalidInput, err)
}
return string(out), nil
case BASE64_URL:
if err := options.rejectUnknown(); err != nil {
return "", err
}
out, err := base64x.DecodeURL(data)
if err != nil {
return "", fmt.Errorf("%w: %w", ErrInvalidInput, err)
}
return string(out), nil
case HEX:
if err := options.rejectUnknown(); err != nil {
return "", err
}
out, err := hexx.Decode(data)
if err != nil {
return "", fmt.Errorf("%w: %w", ErrInvalidInput, err)
}
return string(out), nil
case AES_GCM:
if err := options.rejectUnknown(optAAD); err != nil {
return "", err
}
aad, err := optionalStringOption(encryptType, options, optAAD)
if err != nil {
return "", err
}
ciphertextHex, nonceHex, err := splitAEAD(data)
if err != nil {
return "", err
}
ciphertext, err := hexx.Decode(ciphertextHex)
if err != nil {
return "", fmt.Errorf("%w: 密文解码失败: %w", ErrInvalidInput, err)
}
nonce, err := hexx.Decode(nonceHex)
if err != nil {
return "", fmt.Errorf("%w: nonce 解码失败: %w", ErrInvalidInput, err)
}
out, err := aesx.DecryptGCM(ciphertext, key, nonce, aad)
if err != nil {
return "", classifyDecryptError(err)
}
return string(out), nil
case CHACHA20_POLY1305, XCHACHA20_POLY1305:
if err := options.rejectUnknown(optAAD); err != nil {
return "", err
}
aad, err := optionalStringOption(encryptType, options, optAAD)
if err != nil {
return "", err
}
ciphertextHex, nonceHex, err := splitAEAD(data)
if err != nil {
return "", err
}
ciphertext, err := hexx.Decode(ciphertextHex)
if err != nil {
return "", fmt.Errorf("%w: 密文解码失败: %w", ErrInvalidInput, err)
}
nonce, err := hexx.Decode(nonceHex)
if err != nil {
return "", fmt.Errorf("%w: nonce 解码失败: %w", ErrInvalidInput, err)
}
var plaintext []byte
if encryptType == CHACHA20_POLY1305 {
plaintext, err = chachax.Decrypt(key, ciphertext, nonce, aad)
} else {
plaintext, err = chachax.DecryptX(key, ciphertext, nonce, aad)
}
if err != nil {
return "", classifyDecryptError(err)
}
return string(plaintext), nil
case DES_ECB, DES_CBC, DES_CFB, DES_OFB, DES_CTR,
TDES_ECB, TDES_CBC, TDES_CFB, TDES_OFB, TDES_CTR,
AES_ECB, AES_CBC, AES_CFB, AES_OFB, AES_CTR,
BLOWFISH_ECB, BLOWFISH_CBC, BLOWFISH_CFB, BLOWFISH_OFB, BLOWFISH_CTR:
if err := options.rejectUnknown(optIV); err != nil {
return "", err
}
suite, mode, err := suiteAndMode(encryptType)
if err != nil {
return "", err
}
iv, hasIV, err := optionalBytesValue(encryptType, options, optIV)
if err != nil {
return "", err
}
var plaintext []byte
if hasIV {
plaintext, err = suite.DecryptWithIVHex(mode, data, key, iv)
} else {
plaintext, err = suite.DecryptHex(mode, data, key)
}
if err != nil {
return "", classifyDecryptError(err)
}
return string(plaintext), nil
case RSA, RSA_OAEP:
if err := options.rejectUnknown(optPrivateKeyFile); err != nil {
return "", err
}
priv, err := resolvePrivateKey(key, options)
if err != nil {
return "", err
}
ciphertext, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return "", fmt.Errorf("%w: Base64 解码失败: %w", ErrInvalidInput, err)
}
plaintext, err := rsax.DecryptOAEPLong(priv, ciphertext)
if err != nil {
return "", err
}
return string(plaintext), nil
}
return "", fmt.Errorf("%w: %d", ErrUnsupportedType, int(encryptType))
}
// GenerateKey 生成随机密钥或盐值。
//
// 返回的字节可以直接作为 Encrypt/Decrypt 的 key 参数,不存在任何编码转换——
// 这正是历史版本最严重缺陷的修复点:旧版返回十六进制文本,长度翻倍后任何
// 加密调用都会因密钥长度非法而失败。
//
// 对 RSA,返回 PKCS#8 私钥的 PEM 内容。若同时给出 privateKeyFile 与
// publicKeyFile 选项,还会把密钥对写入这两个文件。
func GenerateKey(encryptType EncryptType, options Options) ([]byte, error) {
if !encryptType.Valid() {
return nil, fmt.Errorf("%w: %d", ErrUnsupportedType, int(encryptType))
}
if options == nil {
options = Options{}
}
switch encryptType {
case DES_ECB, DES_CBC, DES_CFB, DES_OFB, DES_CTR:
if err := options.rejectUnknown(); err != nil {
return nil, err
}
return desx.GenerateKey()
case TDES_ECB, TDES_CBC, TDES_CFB, TDES_OFB, TDES_CTR:
if err := options.rejectUnknown(); err != nil {
return nil, err
}
return tdesx.GenerateKey()
case AES_ECB, AES_CBC, AES_CFB, AES_OFB, AES_CTR, AES_GCM:
if err := options.rejectUnknown(optKeyLength); err != nil {
return nil, err
}
keyLength, err := optionalInt(encryptType, options, optKeyLength, aesx.KeySize256)
if err != nil {
return nil, err
}
return aesx.GenerateKey(keyLength)
case BLOWFISH_ECB, BLOWFISH_CBC, BLOWFISH_CFB, BLOWFISH_OFB, BLOWFISH_CTR:
if err := options.rejectUnknown(optKeyLength); err != nil {
return nil, err
}
keyLength, err := optionalInt(encryptType, options, optKeyLength, blowfishx.DefaultKeySize)
if err != nil {
return nil, err
}
return blowfishx.GenerateKey(keyLength)
case CHACHA20_POLY1305, XCHACHA20_POLY1305:
if err := options.rejectUnknown(); err != nil {
return nil, err
}
return chachax.GenerateKey()
case HMAC_SHA1, HMAC_SHA256, HMAC_SHA384, HMAC_SHA512:
if err := options.rejectUnknown(optKeyLength); err != nil {
return nil, err
}
keyLength, err := optionalInt(encryptType, options, optKeyLength, hmacx.DefaultKeyLength)
if err != nil {
return nil, err
}
if keyLength < hmacx.MinKeyLength {
return nil, fmt.Errorf("encryptx: HMAC 密钥长度 %d 小于最小值 %d 字节",
keyLength, hmacx.MinKeyLength)
}
return utilsx.RandomBytes(keyLength)
case PBKDF2, HKDF, SCRYPT, ARGON2ID:
if err := options.rejectUnknown(optSaltLength); err != nil {
return nil, err
}
saltLength, err := optionalInt(encryptType, options, optSaltLength, pbkdf2x.DefaultSaltLength)
if err != nil {
return nil, err
}
return utilsx.RandomBytes(saltLength)
case RSA, RSA_OAEP:
if err := options.rejectUnknown(optBits, optPrivateKeyFile, optPublicKeyFile); err != nil {
return nil, err
}
bits, err := optionalInt(encryptType, options, optBits, rsax.DefaultKeyBits)
if err != nil {
return nil, err
}
privatePEM, publicPEM, err := rsax.GenerateKeyPairPEM(bits)
if err != nil {
return nil, err
}
if err := maybeSaveKeyPair(privatePEM, publicPEM, options); err != nil {
return nil, err
}
return privatePEM, nil
case MD5, SHA1, SHA224, SHA256, SHA384, SHA512,
SHA3_224, SHA3_256, SHA3_384, SHA3_512,
BLAKE2B_256, BLAKE2S_256, RIPEMD160:
return nil, fmt.Errorf("encryptx: %s 是哈希算法, 不需要密钥", encryptType)
case BCRYPT:
return nil, fmt.Errorf("encryptx: bcrypt 的盐值内嵌在哈希结果中, 无需单独生成; 请直接调用 HashPassword")
case BASE64, BASE64_URL, HEX:
return nil, fmt.Errorf("encryptx: %s 是编码而非加密, 不需要密钥", encryptType)
}
return nil, fmt.Errorf("%w: %d", ErrUnsupportedType, int(encryptType))
}
// GenerateKeyPair 生成非对称密钥对,返回 PEM 编码的私钥与公钥。
//
// 对 RSA 可用 options["bits"] 指定位数(默认 2048,最小 2048);Ed25519 与
// ECDSA 忽略该选项(曲线由 SignType 决定)。
func GenerateKeyPair(signType SignType, options Options) (privatePEM, publicPEM []byte, err error) {
if !signType.Valid() {
return nil, nil, fmt.Errorf("%w: 签名算法 %d", ErrUnsupportedType, int(signType))
}
if options == nil {
options = Options{}
}
switch signType {
case ED25519:
if err := options.rejectUnknown(); err != nil {
return nil, nil, err
}
return ed25519x.GenerateKeyPEM()
case ECDSA_P256, ECDSA_P384, ECDSA_P521:
if err := options.rejectUnknown(); err != nil {
return nil, nil, err
}
return ecdsax.GenerateKeyPEM(curveForSignType(signType))
case RSA_PSS, RSA_PKCS1V15:
if err := options.rejectUnknown(optBits); err != nil {
return nil, nil, err
}
bits, err := optionalInt(RSA, options, optBits, rsax.DefaultKeyBits)
if err != nil {
return nil, nil, err
}
return rsax.GenerateKeyPairPEM(bits)
}
return nil, nil, fmt.Errorf("%w: 签名算法 %d", ErrUnsupportedType, int(signType))
}
// DeriveKey 从口令/密钥材料派生密钥,**同时返回实际使用的盐值**。
//
// 这是密钥派生推荐的入口:盐值必须与派生结果一起保存,否则无法复现。历史版本
// 的门面只返回密钥而丢弃盐值,使派生结果永远无法复现。
//
// 支持 PBKDF2、HKDF、SCRYPT、ARGON2ID。HKDF 用于高熵密钥材料的派生与分离,
// 不适合低熵口令;口令存储请用 ARGON2ID 或 SCRYPT,或直接使用 HashPassword。
//
// 若 options 提供了 salt 就使用它,否则生成随机盐并随返回值一并给出。
func DeriveKey(encryptType EncryptType, secret []byte, options Options) (key, salt []byte, err error) {
if options == nil {
options = Options{}
}
if len(secret) == 0 {
return nil, nil, fmt.Errorf("%w: 派生用的口令/密钥材料不能为空", ErrInvalidInput)
}
switch encryptType {
case PBKDF2:
if err := options.rejectUnknown(optSalt, optSaltLength, optIterations, optKeyLength); err != nil {
return nil, nil, err
}
iterations, err := optionalInt(encryptType, options, optIterations, pbkdf2x.DefaultIterations)
if err != nil {
return nil, nil, err
}
keyLength, err := optionalInt(encryptType, options, optKeyLength, pbkdf2x.DefaultKeyLength)
if err != nil {
return nil, nil, err
}
saltLength, err := optionalInt(encryptType, options, optSaltLength, pbkdf2x.DefaultSaltLength)
if err != nil {
return nil, nil, err
}
return deriveWithOptionalSalt(encryptType, options, secret,
func(s []byte) ([]byte, error) {
return pbkdf2x.DeriveKey(secret, s, iterations, keyLength)
},
func() ([]byte, []byte, error) {
return pbkdf2x.DeriveKeyWithRandomSalt(secret, saltLength, iterations, keyLength)
})
case HKDF:
if err := options.rejectUnknown(optSalt, optInfo, optKeyLength); err != nil {
return nil, nil, err
}
keyLength, err := optionalInt(encryptType, options, optKeyLength, hkdfx.DefaultKeyLength)
if err != nil {
return nil, nil, err
}
info, err := optionalBytes(encryptType, options, optInfo)
if err != nil {
return nil, nil, err
}
salt, provided, err := optionalBytesValue(encryptType, options, optSalt)
if err != nil {
return nil, nil, err
}
if !provided {
// HKDF 允许空盐(标准等价于全零盐),无需强制随机化。
salt = nil
}
key, err = hkdfx.DeriveKey(secret, salt, info, keyLength)
if err != nil {
return nil, nil, err
}
return key, salt, nil
case SCRYPT:
if err := options.rejectUnknown(optSalt, optSaltLength, optScryptN, optScryptR, optScryptP, optKeyLength); err != nil {
return nil, nil, err
}
n, err := optionalInt(encryptType, options, optScryptN, scryptx.DefaultN)
if err != nil {
return nil, nil, err
}
r, err := optionalInt(encryptType, options, optScryptR, scryptx.DefaultR)
if err != nil {
return nil, nil, err
}
p, err := optionalInt(encryptType, options, optScryptP, scryptx.DefaultP)
if err != nil {
return nil, nil, err
}
keyLength, err := optionalInt(encryptType, options, optKeyLength, scryptx.DefaultKeyLength)
if err != nil {
return nil, nil, err
}
saltLength, err := optionalInt(encryptType, options, optSaltLength, scryptx.DefaultSaltLength)
if err != nil {
return nil, nil, err
}
return deriveWithOptionalSalt(encryptType, options, secret,
func(s []byte) ([]byte, error) {
return scryptx.DeriveKey(secret, s, n, r, p, keyLength)
},
func() ([]byte, []byte, error) {
return scryptx.DeriveKeyWithRandomSalt(secret, saltLength, n, r, p, keyLength)
})
case ARGON2ID:
if err := options.rejectUnknown(optSalt, optSaltLength, optArgon2Time, optArgon2Memory, optArgon2Threads, optKeyLength); err != nil {
return nil, nil, err
}
timeCost, err := optionalInt(encryptType, options, optArgon2Time, argon2x.DefaultTime)
if err != nil {
return nil, nil, err
}
memory, err := optionalInt(encryptType, options, optArgon2Memory, argon2x.DefaultMemory)
if err != nil {
return nil, nil, err
}
threads, err := optionalInt(encryptType, options, optArgon2Threads, argon2x.DefaultThreads)
if err != nil {
return nil, nil, err
}
keyLength, err := optionalInt(encryptType, options, optKeyLength, argon2x.DefaultKeyLength)
if err != nil {
return nil, nil, err
}
saltLength, err := optionalInt(encryptType, options, optSaltLength, argon2x.DefaultSaltLength)
if err != nil {
return nil, nil, err
}
return deriveWithOptionalSalt(encryptType, options, secret,
func(s []byte) ([]byte, error) {
return argon2x.DeriveKey(secret, s,
uint32(timeCost), uint32(memory), uint8(threads), uint32(keyLength))
},
func() ([]byte, []byte, error) {
return argon2x.DeriveKeyWithRandomSalt(secret, saltLength,
uint32(timeCost), uint32(memory), uint8(threads), uint32(keyLength))
})
}
return nil, nil, fmt.Errorf("%w: %s 不是密钥派生算法", ErrUnsupportedType, encryptType)
}
// Sign 使用非对称私钥对数据签名,返回标准 Base64 编码的签名。
//
// key 必须是 PEM 编码的私钥内容,也可以用 options["privateKeyFile"] 指定文件路径。
func Sign(signType SignType, data string, key []byte, options Options) (string, error) {
if !signType.Valid() {
return "", fmt.Errorf("%w: 签名算法 %d", ErrUnsupportedType, int(signType))
}
if options == nil {
options = Options{}
}
if err := options.rejectUnknown(optPrivateKeyFile); err != nil {
return "", err
}
keyPEM, err := resolveKeyMaterial(key, options, optPrivateKeyFile, "私钥")
if err != nil {
return "", err
}
switch signType {
case ED25519:
priv, err := ed25519x.ParsePrivateKeyPEM(keyPEM)
if err != nil {
return "", err
}
return ed25519x.SignToBase64(priv, []byte(data))
case ECDSA_P256, ECDSA_P384, ECDSA_P521:
priv, err := ecdsax.ParsePrivateKeyPEM(keyPEM)
if err != nil {
return "", err
}
if priv.Curve != curveForSignType(signType) {
return "", fmt.Errorf("encryptx: 私钥曲线与 %s 不匹配", signType)
}
return ecdsax.SignToBase64(priv, []byte(data))
case RSA_PSS, RSA_PKCS1V15:
priv, err := rsax.ParsePrivateKeyPEM(keyPEM)
if err != nil {
return "", err
}
var signature []byte
if signType == RSA_PSS {
signature, err = rsax.SignPSS(priv, []byte(data))
} else {
signature, err = rsax.SignPKCS1v15(priv, []byte(data))
}
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(signature), nil
}
return "", fmt.Errorf("%w: 签名算法 %d", ErrUnsupportedType, int(signType))
}
// Verify 校验签名:有效返回 nil,无效返回错误。
//
// key 必须是 PEM 编码的公钥内容,也可以用 options["publicKeyFile"] 指定文件路径。
func Verify(signType SignType, data string, signature string, key []byte, options Options) error {
if !signType.Valid() {
return fmt.Errorf("%w: 签名算法 %d", ErrUnsupportedType, int(signType))
}
if options == nil {
options = Options{}
}
if err := options.rejectUnknown(optPublicKeyFile); err != nil {
return err
}
keyPEM, err := resolveKeyMaterial(key, options, optPublicKeyFile, "公钥")
if err != nil {
return err
}
switch signType {
case ED25519:
pub, err := ed25519x.ParsePublicKeyPEM(keyPEM)
if err != nil {
return err
}
return ed25519x.VerifyFromBase64(pub, []byte(data), signature)
case ECDSA_P256, ECDSA_P384, ECDSA_P521:
pub, err := ecdsax.ParsePublicKeyPEM(keyPEM)
if err != nil {
return err
}
if pub.Curve != curveForSignType(signType) {
return fmt.Errorf("encryptx: 公钥曲线与 %s 不匹配", signType)
}
return ecdsax.VerifyFromBase64(pub, []byte(data), signature)
case RSA_PSS, RSA_PKCS1V15:
pub, err := rsax.ParsePublicKeyPEM(keyPEM)
if err != nil {
return err
}
signatureBytes, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return fmt.Errorf("%w: 签名 Base64 解码失败: %w", ErrInvalidInput, err)
}
if signType == RSA_PSS {
return rsax.VerifyPSS(pub, []byte(data), signatureBytes)
}
return rsax.VerifyPKCS1v15(pub, []byte(data), signatureBytes)
}
return fmt.Errorf("%w: 签名算法 %d", ErrUnsupportedType, int(signType))
}
// HashPassword 对用户口令做单向哈希,返回可直接入库的编码串。
//
// 支持 BCRYPT 与 ARGON2ID:两者都把参数与随机盐编码在结果中,因此不需要另行
// 保存盐值。无兼容性约束时建议使用 ARGON2ID。
func HashPassword(encryptType EncryptType, password []byte, options Options) (string, error) {
if options == nil {
options = Options{}
}
switch encryptType {
case BCRYPT:
if err := options.rejectUnknown(optCost); err != nil {
return "", err
}
return bcryptHash(string(password), options)
case ARGON2ID:
if err := options.rejectUnknown(optArgon2Time, optArgon2Memory, optArgon2Threads, optKeyLength); err != nil {
return "", err
}
return argon2Hash(string(password), options)
}
return "", fmt.Errorf("%w: %s 不是口令哈希算法", ErrUnsupportedType, encryptType)
}
// VerifyPassword 校验口令是否与 HashPassword 产生的编码串匹配。
//
// 返回值语义:编码串本身非法时返回 error,口令不匹配时返回 (false, nil)。
func VerifyPassword(encryptType EncryptType, password []byte, encoded string, options Options) (bool, error) {
if options == nil {
options = Options{}
}
switch encryptType {
case BCRYPT:
if err := options.rejectUnknown(); err != nil {
return false, err
}
return bcryptx.VerifyPassword(password, encoded)
case ARGON2ID:
if err := options.rejectUnknown(); err != nil {
return false, err
}
return argon2x.VerifyPassword(password, encoded)
}
return false, fmt.Errorf("%w: %s 不是口令哈希算法", ErrUnsupportedType, encryptType)
}
// ---- 内部辅助 ----
// suiteAndMode 把分组密码算法枚举映射为 (套件, 模式)。
func suiteAndMode(encryptType EncryptType) (*cipherx.Suite, cipherx.Mode, error) {
switch encryptType {
case DES_ECB:
return desx.Suite, cipherx.ECB, nil
case DES_CBC:
return desx.Suite, cipherx.CBC, nil
case DES_CFB:
return desx.Suite, cipherx.CFB, nil
case DES_OFB:
return desx.Suite, cipherx.OFB, nil
case DES_CTR:
return desx.Suite, cipherx.CTR, nil
case TDES_ECB:
return tdesx.Suite, cipherx.ECB, nil
case TDES_CBC:
return tdesx.Suite, cipherx.CBC, nil
case TDES_CFB:
return tdesx.Suite, cipherx.CFB, nil
case TDES_OFB:
return tdesx.Suite, cipherx.OFB, nil
case TDES_CTR:
return tdesx.Suite, cipherx.CTR, nil
case AES_ECB:
return aesx.Suite, cipherx.ECB, nil
case AES_CBC:
return aesx.Suite, cipherx.CBC, nil
case AES_CFB:
return aesx.Suite, cipherx.CFB, nil
case AES_OFB:
return aesx.Suite, cipherx.OFB, nil
case AES_CTR:
return aesx.Suite, cipherx.CTR, nil
case BLOWFISH_ECB:
return blowfishx.Suite, cipherx.ECB, nil
case BLOWFISH_CBC:
return blowfishx.Suite, cipherx.CBC, nil
case BLOWFISH_CFB:
return blowfishx.Suite, cipherx.CFB, nil
case BLOWFISH_OFB:
return blowfishx.Suite, cipherx.OFB, nil
case BLOWFISH_CTR:
return blowfishx.Suite, cipherx.CTR, nil
}
return nil, 0, fmt.Errorf("%w: %s 不是分组密码算法", ErrUnsupportedType, encryptType)
}
// splitAEAD 拆分 "密文:nonce" 形式的 AEAD 输出。
func splitAEAD(data string) (ciphertextHex, nonceHex string, err error) {
parts := strings.Split(data, aeadSeparator)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return "", "", fmt.Errorf("%w: AEAD 密文格式应为 \"密文十六进制:nonce十六进制\"", ErrInvalidInput)
}
return parts[0], parts[1], nil
}
// optionalInt 读取整数选项,缺省时返回 defaultValue。
func optionalInt(encryptType EncryptType, options Options, key string, defaultValue int) (int, error) {
value, ok, err := options.Int(key)
if err != nil {
return 0, fmt.Errorf("%s: %w", encryptType, err)
}
if !ok {
return defaultValue, nil
}
return value, nil
}
// optionalStringOption 读取字符串选项并按原样取字节,缺省返回 nil。
func optionalStringOption(encryptType EncryptType, options Options, key string) ([]byte, error) {
value, ok, err := options.String(key)
if err != nil {
return nil, fmt.Errorf("%s: %w", encryptType, err)
}
if !ok {
return nil, nil
}
return []byte(value), nil
}
// optionalBytes 读取二进制选项(接受 []byte 或十六进制字符串),缺省返回 nil。
func optionalBytes(encryptType EncryptType, options Options, key string) ([]byte, error) {
value, _, err := optionalBytesValue(encryptType, options, key)
return value, err
}
// optionalBytesValue 读取二进制选项,同时报告该键是否存在。
func optionalBytesValue(encryptType EncryptType, options Options, key string) ([]byte, bool, error) {
if _, exists := options[key]; !exists {
return nil, false, nil
}
value, ok, err := options.Bytes(key)
if err != nil {
return nil, false, fmt.Errorf("%s: %w", encryptType, err)
}
return value, ok, nil
}
// decodeOptionBytes 把字符串选项解释为十六进制字节。
//
// 约定字符串一律是十六进制:这样盐值、IV 等参数在配置文件与数据库里都是可读
// 文本,也不存在「这串字符到底是编码还是原文」的歧义。需要传原始字节时请直接
// 传 []byte。
func decodeOptionBytes(key, value string) ([]byte, error) {
decoded, err := hex.DecodeString(strings.TrimSpace(value))
if err != nil {
return nil, fmt.Errorf("%w: 选项 %q 需要十六进制字符串: %w", ErrInvalidOption, key, err)
}
return decoded, nil
}
// hmacHex 按算法计算 HMAC 并返回小写十六进制。
func hmacHex(encryptType EncryptType, message string, key []byte) string {
switch encryptType {
case HMAC_SHA1:
return hmacx.HexSHA1([]byte(message), key)
case HMAC_SHA256:
return hmacx.HexSHA256([]byte(message), key)
case HMAC_SHA384:
return hmacx.HexSHA384([]byte(message), key)
default:
return hmacx.HexSHA512([]byte(message), key)
}
}
// bcryptHash 处理 BCRYPT 的口令哈希。
func bcryptHash(password string, options Options) (string, error) {
cost, err := optionalInt(BCRYPT, options, optCost, bcryptx.DefaultCost)
if err != nil {
return "", err
}
return bcryptx.HashPasswordWithCost([]byte(password), cost)
}
// argon2Hash 处理 ARGON2ID 的口令哈希。
func argon2Hash(password string, options Options) (string, error) {
timeCost, err := optionalInt(ARGON2ID, options, optArgon2Time, argon2x.DefaultTime)
if err != nil {
return "", err
}
memory, err := optionalInt(ARGON2ID, options, optArgon2Memory, argon2x.DefaultMemory)
if err != nil {
return "", err
}
threads, err := optionalInt(ARGON2ID, options, optArgon2Threads, argon2x.DefaultThreads)
if err != nil {
return "", err
}
keyLength, err := optionalInt(ARGON2ID, options, optKeyLength, argon2x.DefaultKeyLength)
if err != nil {
return "", err
}
return argon2x.HashPasswordWithParams([]byte(password),
uint32(timeCost), uint32(memory), uint8(threads), uint32(keyLength))
}
// deriveWithOptionalSalt 统一处理「给了盐就用给定的、没给就随机生成」两条分支,
// 保证两种路径都返回实际使用的盐值。
func deriveWithOptionalSalt(
encryptType EncryptType,
options Options,
_ []byte,
withSalt func(salt []byte) ([]byte, error),
withRandomSalt func() (key, salt []byte, err error),
) (key, salt []byte, err error) {
providedSalt, provided, err := optionalBytesValue(encryptType, options, optSalt)
if err != nil {
return nil, nil, err
}
if provided {
key, err := withSalt(providedSalt)
if err != nil {
return nil, nil, err
}
return key, providedSalt, nil
}
return withRandomSalt()
}
// deriveHex 让 Encrypt 支持密钥派生算法。
//
// 为避免重蹈「盐值被丢弃」的覆辙,本路径要求除 HKDF 之外的算法显式提供 salt
// 选项;需要自动生成盐值并把它取回时,请调用 DeriveKey。
//
// HKDF 是例外:它的盐值可以为空(标准规定等价于全零盐),且其输入本就是高熵
// 密钥材料,因此允许缺省,与 DeriveKey 的行为保持一致。
func deriveHex(encryptType EncryptType, data string, options Options) (string, error) {
if _, ok, err := optionalBytesValue(encryptType, options, optSalt); err != nil {
return "", err
} else if !ok && encryptType != HKDF {
return "", fmt.Errorf("%w: %s 需要 %q 选项才能复现结果; "+
"若希望自动生成盐值并同时取回, 请使用 DeriveKey", ErrMissingOption, encryptType, optSalt)
}
derived, _, err := DeriveKey(encryptType, []byte(data), options)
if err != nil {
return "", err
}
return hexx.Encode(derived), nil
}
// checkKeyUsage 统一校验密钥的"有无"是否符合算法预期。
//
// 需要密钥的算法缺密钥 → ErrMissingKey;不需要密钥的算法却传了密钥 →
// ErrUnexpectedKey。后者尤其重要:历史版本会静默忽略多余的密钥,让调用方
// 误以为 Base64 编码或哈希提供了机密性保护。
func checkKeyUsage(encryptType EncryptType, key []byte, options Options) error {
if encryptType.NeedsKey() {
if len(key) == 0 && !keyMayComeFromFile(options) {
return fmt.Errorf("%w: %s", ErrMissingKey, encryptType)
}
return nil
}
if len(key) > 0 {
return fmt.Errorf("%w: %s 不使用密钥, 却传入了 %d 字节的 key",
ErrUnexpectedKey, encryptType, len(key))
}
return nil
}
// classifyDecryptError 让门面的错误分类保持自洽。
//
// 密文的编码/长度问题归类为 ErrInvalidInput(这些是调用方传错了数据);而认证
// 失败必须保持可区分——若把"被篡改"也归为"输入格式错误",调用方就无法把它和
// 参数写错区分开,安全告警会被淹没在普通错误里。
func classifyDecryptError(err error) error {
if err == nil {
return nil
}
switch {
case errors.Is(err, aesx.ErrAuthenticationFailed),
errors.Is(err, chachax.ErrAuthenticationFailed):
return err
case errors.Is(err, cipherx.ErrCiphertextTooShort),
errors.Is(err, cipherx.ErrNotBlockAligned),
errors.Is(err, cipherx.ErrEmptyCiphertext),
errors.Is(err, cipherx.ErrInvalidIVSize),
errors.Is(err, cipherx.ErrInvalidHex),
errors.Is(err, aesx.ErrInvalidNonceSize),
errors.Is(err, aesx.ErrCiphertextTooShort),
errors.Is(err, chachax.ErrInvalidNonceSize),
errors.Is(err, chachax.ErrCiphertextTooShort):
return fmt.Errorf("%w: %w", ErrInvalidInput, err)
default:
return err
}
}
// keyMayComeFromFile 报告密钥是否可能由选项里的文件路径提供。
//
// 非对称算法的 key 参数允许留空,改用 publicKeyFile / privateKeyFile 指定文件,
// 因此"密钥为空"不能一概判为缺参。
func keyMayComeFromFile(options Options) bool {
if v, ok := options[optPublicKeyFile].(string); ok && v != "" {
return true
}
if v, ok := options[optPrivateKeyFile].(string); ok && v != "" {
return true
}
return false
}
// resolveKeyMaterial 返回密钥 PEM 内容:优先用显式传入的字节,其次按选项读文件。
func resolveKeyMaterial(key []byte, options Options, fileOption, what string) ([]byte, error) {
if len(key) > 0 {
return key, nil
}
path, ok, err := options.String(fileOption)
if err != nil {
return nil, err
}
if !ok || path == "" {
return nil, fmt.Errorf("%w: 需要 %s(PEM 内容, 或 %s 选项指定文件)", ErrMissingKey, what, fileOption)
}
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("encryptx: 读取 %s 文件失败: %w", what, err)
}
return data, nil
}
// resolvePublicKey 解析 RSA 公钥(PEM 内容或文件)。
func resolvePublicKey(key []byte, options Options) (*rsa.PublicKey, error) {
keyPEM, err := resolveKeyMaterial(key, options, optPublicKeyFile, "公钥")
if err != nil {
return nil, err
}
return rsax.ParsePublicKeyPEM(keyPEM)
}
// resolvePrivateKey 解析 RSA 私钥(PEM 内容或文件)。
func resolvePrivateKey(key []byte, options Options) (*rsa.PrivateKey, error) {
keyPEM, err := resolveKeyMaterial(key, options, optPrivateKeyFile, "私钥")
if err != nil {
return nil, err
}
return rsax.ParsePrivateKeyPEM(keyPEM)
}
// maybeSaveKeyPair 在提供了文件路径选项时把密钥对写入磁盘。
func maybeSaveKeyPair(privatePEM, publicPEM []byte, options Options) error {
privatePath, hasPrivate, err := options.String(optPrivateKeyFile)
if err != nil {
return err
}
publicPath, hasPublic, err := options.String(optPublicKeyFile)
if err != nil {
return err
}
if !hasPrivate && !hasPublic {
return nil
}
if !hasPrivate || !hasPublic {
return fmt.Errorf("encryptx: %s 与 %s 必须同时提供", optPrivateKeyFile, optPublicKeyFile)
}
if err := os.WriteFile(privatePath, privatePEM, 0o600); err != nil {
return fmt.Errorf("encryptx: 写入私钥文件失败: %w", err)
}
if err := os.WriteFile(publicPath, publicPEM, 0o644); err != nil {
return fmt.Errorf("encryptx: 写入公钥文件失败: %w", err)
}
return nil
}
// curveForSignType 返回签名类型对应的椭圆曲线。
func curveForSignType(signType SignType) elliptic.Curve {
switch signType {
case ECDSA_P256:
return elliptic.P256()
case ECDSA_P384:
return elliptic.P384()
default:
return elliptic.P521()
}
}