1145 lines
36 KiB
Go
1145 lines
36 KiB
Go
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()
|
|||
|
|
}
|
|||
|
|
}
|