Gin+原生JS,实现一套AES+RSA混合加密数据传输流程

从前端,到后端,除了Gin框架本身,完全不依赖第三方库,极简实现,无比丝滑。

流程梳理

  1. 客户端生成一个AES密钥(建议存Cookie里)
  2. 服务端生成一对RSA密钥private.pem+public.pem
  3. 服务端开启一个接口,接收任意请求方法都行,将RSA公钥Set-Cookie标头直接塞到客户端浏览器Cookie里面(记得以base64传免得乱套)
  4. 客户端Cookie取出服务端给的RSA公钥进行base64解码
  5. 客户端使用 “经过服务端RSA公钥加密的客户端AES密钥” 加密即将要传输的敏感消息,同时连带AES密钥POST给后端(总共俩数据,依然是base64后端记得处理解码)
  6. 服务端接收到数据时,先使用服务端RSA私钥解密接收到的俩数据之中的客户端AES密钥
  7. 服务端创建一个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>
暂无评论

发送评论 编辑评论


				
上一篇
下一篇