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() } }