从前端,到后端,除了Gin框架本身,完全不依赖第三方库,极简实现,无比丝滑。
流程梳理
- 客户端生成一个
AES密钥
(建议存Cookie里) - 服务端生成一对
RSA密钥
(private.pem
+public.pem
) - 服务端开启一个接口,接收任意请求方法都行,将RSA公钥以Set-Cookie标头直接塞到客户端浏览器
Cookie
里面(记得以base64
传免得乱套) - 客户端从
Cookie
取出服务端给的RSA公钥
进行base64
解码 - 客户端使用 “经过服务端
RSA公钥
加密的客户端AES密钥
” 加密即将要传输的敏感消息,同时连带AES密钥POST给后端(总共俩数据,依然是base64
后端记得处理解码) - 服务端接收到数据时,先使用服务端
RSA私钥
来解密接收到的俩数据之中的客户端AES密钥
- 服务端创建一个GCM模式实例,并从加密数据中提取nonce(初始化向量)和密文。使用GCM模式实例的
Open
方法来解密数据,得到原始的明文数据。
OK上代码
Gin后端
/util/create_rsa.go
生成RSA公钥+私钥,存到本地,如果已存在或自己在其他地方生成则可以忽略。
package util
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"path/filepath"
)
// 调用辅助函数生成RSA密钥对保存到文件
func GenerateAndSaveRSAKeyPair(savePath string, bits int) error {
priv, pub, err := generateRSAKeyPair(bits)
if err != nil {
return err
}
privPath, pubPath := filepath.Join(savePath, "private.pem"), filepath.Join(savePath, "public.pem")
if err := saveKeyToFile(priv, privPath, "RSA PRIVATE KEY"); err != nil {
return err
}
return saveKeyToFile(pub, pubPath, "RSA PUBLIC KEY")
}
// 生成RSA密钥对
func generateRSAKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) {
priv, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return nil, nil, err
}
return priv, &priv.PublicKey, nil
}
// 不同类型秘钥生成辅助函数
func saveKeyToFile(key interface{}, path, pemType string) error {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
var pemBlock *pem.Block
switch k := key.(type) {
case *rsa.PrivateKey:
privBytes := x509.MarshalPKCS1PrivateKey(k)
pemBlock = &pem.Block{Type: pemType, Bytes: privBytes}
case *rsa.PublicKey:
pubBytes, err := x509.MarshalPKIXPublicKey(k)
if err != nil {
return err
}
pemBlock = &pem.Block{Type: pemType, Bytes: pubBytes}
default:
return fmt.Errorf("unsupported key type: %T", key)
}
return pem.Encode(file, pemBlock)
}
main.go
//主程序
package main
import (
"a/middleware"
"a/util"
"crypto/aes"
"crypto/cipher"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"github.com/gin-gonic/gin"
)
var rsaPrivateKey *rsa.PrivateKey
var rsaPublicKey *rsa.PublicKey
// RSA密钥本地存放文件夹
var keyPath = "./key"
// 从文件中加载密钥
func loadKeyFromFile(path string) ([]byte, error) {
return ioutil.ReadFile(path)
}
// 检查并加载密钥
func checkAndLoadKeys() error {
if err := loadRSAPrivateKey(); err != nil {
return err
}
return loadRSAPublicKey()
}
// 加载RSA私钥
func loadRSAPrivateKey() error {
keyBytes, err := loadKeyFromFile(keyPath + "/private.pem")
if err != nil {
return err
}
block, _ := pem.Decode(keyBytes)
rsaPrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
return err
}
// 加载RSA公钥
func loadRSAPublicKey() error {
keyBytes, err := loadKeyFromFile(keyPath + "/public.pem")
if err != nil {
return err
}
block, _ := pem.Decode(keyBytes)
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
rsaPublicKey = publicKey.(*rsa.PublicKey)
return err
}
// 将公钥转换为Base64编码的字符串
func publicKeyToBase64(publicKey *rsa.PublicKey) (string, error) {
publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
return base64.StdEncoding.EncodeToString(publicKeyBytes), err
}
// 解密AES密钥
func decryptAESKey(encryptedAESKey []byte) ([]byte, error) {
decryptedAESKey, err := rsa.DecryptOAEP(sha256.New(), nil, rsaPrivateKey, encryptedAESKey, nil)
return decryptedAESKey, err
}
// 接口函数-发布RSA公钥, 客户端请求此接口自动获取RSA公钥存入Cookie
func getPublicKeyHandler(c *gin.Context) {
pubKeyPemBase64, _ := publicKeyToBase64(rsaPublicKey)
c.SetCookie("publicKey", pubKeyPemBase64, 3600, "/", "", false, false)
c.JSON(200, gin.H{"code": 200, "rsa_pub_key": pubKeyPemBase64})
}
// 接口函数-客户端发送给服务端密文
func postDecryptHandler(c *gin.Context) {
// 读取JSON请求体键值对
encryptedAESKey, encryptedData := c.PostForm("aes_key"), c.PostForm("ciphertext")
decodedEncryptedAESKey, _ := base64.StdEncoding.DecodeString(encryptedAESKey)
decodedEncryptedData, _ := base64.StdEncoding.DecodeString(encryptedData)
aesKey, _ := decryptAESKey(decodedEncryptedAESKey)
block, _ := aes.NewCipher(aesKey)
gcm, _ := cipher.NewGCM(block)
nonceSize := gcm.NonceSize()
nonce, ciphertext := decodedEncryptedData[:nonceSize], decodedEncryptedData[nonceSize:]
decryptedData, _ := gcm.Open(nil, nonce, ciphertext, nil)
// 响应解密成功的数据
c.JSON(200, gin.H{"code": 200, "raw_data": string(decryptedData)})
}
func main() {
r := gin.Default()
// 生成并保存RSA密钥对,指定密钥长度为2048位
if err := util.GenerateAndSaveRSAKeyPair(keyPath, 2048); err != nil {
log.Fatalf("生成密钥失败: %v", err)
}
if err := checkAndLoadKeys(); err != nil {
panic(err)
}
// 跨域中间件, 自行安排
r.Use(middleware.CORS())
// 设置路由
r.GET("/get", getPublicKeyHandler)
r.POST("/post", postDecryptHandler)
fmt.Print("\n>> 服务启动于 http://localhost:8077\n\n")
r.Run(":8077")
}
客户端(HTML)
放一个输入框、一个发送按钮,在控制台看响应
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户端加密示例</title>
<script>
const util = {
str2ab: str => new Uint8Array(str.split('').map(c => c.charCodeAt(0))).buffer,
ab2str: buf => String.fromCharCode(...new Uint8Array(buf)),
ab2base64: buf => btoa(String.fromCharCode(...new Uint8Array(buf))),
base642ab: base64 => Uint8Array.from(atob(base64), c => c.charCodeAt(0)).buffer,
};
async function encryptMessage(message) {
const aesKey = await crypto.subtle.generateKey({name: "AES-GCM", length: 256}, true, ["encrypt", "decrypt"]);
const publicKeyResponse = await fetch("/get");
const publicKeyPEM = (await publicKeyResponse.json()).publicKey;
const publicKey = await crypto.subtle.importKey("spki", util.base642ab(publicKeyPEM), {name: "RSA-OAEP", hash: "SHA-256"}, true, ["encrypt"]);
const exportedAESKey = await crypto.subtle.exportKey("raw", aesKey);
const encryptedAESKey = await crypto.subtle.encrypt({name: "RSA-OAEP"}, publicKey, exportedAESKey);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedMessage = await crypto.subtle.encrypt({name: "AES-GCM", iv}, aesKey, new TextEncoder().encode(message));
const ivAndEncryptedMessage = new Uint8Array(iv.length + encryptedMessage.byteLength);
ivAndEncryptedMessage.set(iv);
ivAndEncryptedMessage.set(new Uint8Array(encryptedMessage), iv.length);
const formData = new FormData();
formData.append("encrypted_aes_key", util.ab2base64(encryptedAESKey));
formData.append("encrypted_data", util.ab2base64(ivAndEncryptedMessage));
await fetch("/post", {method: "POST", body: formData});
}
</script>
</head>
<body>
<h1>客户端RSA+AES混合加密示例</h1>
<form onsubmit="event.preventDefault(); encryptMessage(message.value)">
<label for="message">加密前的消息:</label>
<input type="text" id="message" name="message">
<button type="submit">加密并发送</button>
</form>
</body>
</html>