Go安全编程:AES对称加密与RSA非对称加密

📅 发布时间:2026/8/17 19:06:27
Go安全编程:AES对称加密与RSA非对称加密 Go安全编程:AES对称加密与RSA非对称加密摘要: 本篇讲解Go语言加密实战使用crypto/aes实现CBC和GCM两种模式的对称加密使用crypto/rsa完成公钥加密和数字签名验证分享ECB模式不安全和IV复用导致密文泄露的踩坑经验对比AES、RSA、AESRSA混合加密三种方案的性能与安全场景。开篇故事去年做一个医疗数据交换平台甲方要求所有传输的病历数据必须加密。第一版我图省事用的AES ECB模式加密完直接把密文塞进HTTP body传输。安全评审时被驳回评审说ECB模式对相同明文加密出相同密文攻击者能通过密文规律推断出业务数据。我重新翻Go的crypto/aes文档发现标准库压根没提供ECB模式我之前是手写循环逐块加密的。换成GCM模式后加了随机nonce和认证标签密文每次都不一样评审才通过。后来对接第三方支付对方用RSA加密AES密钥又踩了IV复用的坑。这篇把AES和RSA两套加密体系在Go里的正确用法写清楚。一、AES对称加密CBC模式AES是对称加密加密和解密用同一把密钥。Go标准库crypto/aes提供了AES的块加密实现但块加密只能处理16字节的数据实际使用需要配合分组模式。CBC是最常见的分组模式之一。packagecryptotoolimport(crypto/aescrypto/ciphercrypto/randerrorsio)// AESEncryptCBC 使用AES-CBC模式加密数据// key必须是16/24/32字节对应AES-128/192/256// 返回IV密文IV需要随密文一起传输funcAESEncryptCBC(key,plaintext[]byte)([]byte,error){// 创建AES块加密器block,err:aes.NewCipher(key)iferr!nil{returnnil,err}// CBC模式需要填充使明文长度为块大小的整数倍// 这里使用PKCS7填充blockSize:block.BlockSize()padded:pkcs7Pad(plaintext,blockSize)// 生成随机IV每次加密都不同// IV不需要保密但必须不可预测ciphertext:make([]byte,blockSizelen(padded))iv:ciphertext[:blockSize]if_,err:io.ReadFull(rand.Reader,iv);err!nil{returnnil,err}// 创建CBC加密器并加密mode:cipher.NewCBCEncrypter(block,iv)mode.CryptBlocks(ciphertext[blockSize:],padded)returnciphertext,nil}// AESDecryptCBC 使用AES-CBC模式解密数据// ciphertext必须是IV密文格式funcAESDecryptCBC(key,ciphertext[]byte)([]byte,error){// 创建AES块加密器block,err:aes.NewCipher(key)iferr!nil{returnnil,err}blockSize:block.BlockSize()iflen(ciphertext)blockSize{returnnil,errors.New(密文太短)}// 分离IV和实际密文iv:ciphertext[:blockSize]ciphertextciphertext[blockSize:]// 长度必须是块大小的整数倍iflen(ciphertext)%blockSize!0{returnnil,errors.New(密文长度不合法)}// 创建CBC解密器并解密mode:cipher.NewCBCDecrypter(block,iv)plaintext:make([]byte,len(ciphertext))mode.CryptBlocks(plaintext,ciphertext)// 去除PKCS7填充returnpkcs7Unpad(plaintext,blockSize)}// pkcs7Pad PKCS7填充funcpkcs7Pad(data[]byte,blockSizeint)[]byte{padding:blockSize-len(data)%blockSize padText:make([]byte,padding)fori:rangepadText{padText[i]byte(padding)}returnappend(data,padText...)}// pkcs7Unpad 去除PKCS7填充funcpkcs7Unpad(data[]byte,blockSizeint)([]byte,error){length:len(data)iflength0{returnnil,errors.New(空数据)}padding:int(data[length-1])ifpadding1||paddingblockSize{returnnil,errors.New(填充不合法)}returndata[:length-padding],nil}CBC模式的关键是IV必须随机生成且不可预测。IV本身不保密可以明文跟密文一起传输。但如果IV固定或可预测攻击者能通过已知明文攻击推断出密钥信息。二、AES-GCM模式与RSA非对称加密GCM模式比CBC更安全它同时提供加密和认证。加密后的密文带一个认证标签解密时先验证标签验证通过才解密。这样即使密文被篡改解密也会失败防止了CBC模式下的密文篡改攻击。packagecryptotoolimport(cryptocrypto/aescrypto/ciphercrypto/randcrypto/rsacrypto/sha256crypto/x509encoding/pemerrorsio)// AESEncryptGCM 使用AES-GCM模式加密// GCM自带认证比CBC更安全推荐使用funcAESEncryptGCM(key,plaintext[]byte)([]byte,error){// 创建AES块加密器block,err:aes.NewCipher(key)iferr!nil{returnnil,err}// 包装成GCM模式// GCM提供加密认证防篡改aesgcm,err:cipher.NewGCM(block)iferr!nil{returnnil,err}// 生成随机nonceGCM的nonce长度固定为12字节nonce:make([]byte,aesgcm.NonceSize())if_,err:io.ReadFull(rand.Reader,nonce);err!nil{returnnil,err}// Seal方法: 加密并附加认证标签// 返回nonce密文标签ciphertext:aesgcm.Seal(nonce,nonce,plaintext,nil)returnciphertext,nil}// AESDecryptGCM 使用AES-GCM模式解密funcAESDecryptGCM(key,ciphertext[]byte)([]byte,error){block,err:aes.NewCipher(key)iferr!nil{returnnil,err}aesgcm,err:cipher.NewGCM(block)iferr!nil{returnnil,err}nonceSize:aesgcm.NonceSize()iflen(ciphertext)nonceSize{returnnil,errors.New(密文太短)}// 分离nonce和实际密文nonce,ciphertext:ciphertext[:nonceSize],ciphertext[nonceSize:]// Open方法: 验证标签并解密篡改会报错returnaesgcm.Open(nil,nonce,ciphertext,nil)}// RSAEncrypt 使用RSA公钥加密数据// 适合加密小数据如AES密钥funcRSAEncrypt(publicKeyPEM[]byte,plaintext[]byte)([]byte,error){// 解析PEM格式的公钥block,_:pem.Decode(publicKeyPEM)ifblocknil{returnnil,errors.New(公钥PEM解析失败)}pub,err:x509.ParsePKIXPublicKey(block.Bytes)iferr!nil{returnnil,err}rsaPub,ok:pub.(*rsa.PublicKey)if!ok{returnnil,errors.New(不是RSA公钥)}// RSA加密使用OAEP填充更安全returnrsa.EncryptOAEP(sha256.New(),rand.Reader,rsaPub,plaintext,nil,)}// RSASign 使用RSA私钥生成数字签名// 用于验证数据完整性和身份认证funcRSASign(privateKeyPEM[]byte,data[]byte)([]byte,error){// 解析PEM格式的私钥block,_:pem.Decode(privateKeyPEM)ifblocknil{returnnil,errors.New(私钥PEM解析失败)}// 支持PKCS1和PKCS8两种格式varpriv*rsa.PrivateKeyvarerrerror// 先尝试PKCS1格式ifkey,err:x509.ParsePKCS1PrivateKey(block.Bytes);errnil{privkey}elseifkey,err:x509.ParsePKCS8PrivateKey(block.Bytes);errnil{// 再尝试PKCS8格式ifrsaKey,ok:key.(*rsa.PrivateKey);ok{privrsaKey}}ifprivnil{returnnil,errors.New(私钥解析失败)}// 对数据做SHA256哈希后签名hashed:sha256.Sum256(data)returnrsa.SignPKCS1v15(rand.Reader,priv,crypto.SHA256,hashed[:])}// RSAVerify 使用RSA公钥验证数字签名funcRSAVerify(publicKeyPEM[]byte,data[]byte,signature[]byte)error{block,_:pem.Decode(publicKeyPEM)ifblocknil{returnerrors.New(公钥PEM解析失败)}pub,err:x509.ParsePKIXPublicKey(block.Bytes)iferr!nil{returnerr}rsaPub,ok:pub.(*rsa.PublicKey)if!ok{returnerrors.New(不是RSA公钥)}// 对数据做同样的SHA256哈希hashed:sha256.Sum256(data)// 验证签名不匹配会返回错误returnrsa.VerifyPKCS1v15(rsaPub,crypto.SHA256,hashed[:],signature)}实际项目里很少单独用RSA加密大量数据。RSA加密慢且有长度限制RSA-2048最多加密245字节。常见做法是用RSA加密AES密钥用AES加密实际数据这就是混合加密。三、独家踩坑:IV复用导致密文泄露这个坑发生在一个对接支付通道的项目里。对方要求用AES-CBC加密请求参数我们为了调试方便把IV硬编码成全零字节。测试环境一切正常上线后发现一个问题同一个订单加密两次密文的前16字节完全一样。CBC模式的加密过程是每一块明文先跟前一块密文异或再加密。第一块明文跟IV异或后加密。如果IV固定为全零相同的明文块就会加密成相同的密文块。攻击者拿到两份密文通过对比就能发现哪些数据是相同的。更严重的是CBC模式IV复用会让攻击者可以通过差分分析恢复出明文。如果攻击者知道一份明文就能推导出另一份明文中对应位置的数据。修复方法是每次加密都生成随机IV。packagecryptotoolimport(crypto/aescrypto/ciphercrypto/randerrorsio)// SecureEncryptCBC 修复后的安全CBC加密// 每次生成随机IV不复用funcSecureEncryptCBC(key,plaintext[]byte)([]byte,error){block,err:aes.NewCipher(key)iferr!nil{returnnil,err}blockSize:block.BlockSize()padded:pkcs7Pad(plaintext,blockSize)// 每次加密都生成新的随机IV// IV不复用是CBC安全的基础ciphertext:make([]byte,blockSizelen(padded))iv:ciphertext[:blockSize]// 使用crypto/rand生成密码学安全的随机数if_,err:io.ReadFull(rand.Reader,iv);err!nil{returnnil,err}mode:cipher.NewCBCEncrypter(block,iv)mode.CryptBlocks(ciphertext[blockSize:],padded)returnciphertext,nil}// GenerateAESKey 生成安全的AES密钥// 密钥必须使用密码学安全的随机数生成funcGenerateAESKey(bitsint)([]byte,error){ifbits!128bits!192bits!256{returnnil,errors.New(密钥长度必须是128/192/256)}key:make([]byte,bits/8)// crypto/rand是密码学安全的随机源// 不能用math/rand生成密钥if_,err:io.ReadFull(rand.Reader,key);err!nil{returnnil,err}returnkey,nil}GCM模式对nonce复用更敏感。GCM的nonce复用不仅泄露明文还会泄露认证密钥攻击者能伪造合法密文。所以GCM模式务必使用随机nonce标准库的Seal方法不自动生成nonce需要我们自己生成。四、对比分析加密方案安全性性能密钥管理适用场景AES-CBC中(需配合随机IV)高对称密钥分发难大数据加密AES-GCM高(自带认证)高对称密钥分发难API数据加密RSA-OAEP高低(慢50-100倍)公钥分发方便加密小数据/密钥AESRSA混合高中(RSA仅加密密钥)兼顾两者优势通信加密标准方案AES加密快适合加密大量数据但密钥分发是个难题通信双方怎么安全地交换AES密钥。RSA解决了密钥分发问题公钥可以公开但加密慢且有长度限制。混合加密结合两者优势用RSA加密AES密钥解决分发问题用AES加密数据解决性能问题。HTTPS底层就是这么做的。总结AES-GCM是Go里对称加密的首选自带认证防篡改比CBC安全。RSA适合加密小数据和数字签名不适合加密大量数据。密钥和IV/nonce必须用crypto/rand生成math/rand不是密码学安全的。IV和nonce绝对不能复用复用会直接破坏加密体系的安全性。下一篇我们聊TLS/HTTPS配置看看怎么在Go服务里正确配置证书和加密传输。