前言

本文通过探讨一种前端加解密方式, 前端使用WebAssembly(wasm)提供公钥加密,后端使用私钥解密,能大大提高破解难度。 wasm使用支持多种语言,我们这里使用golang开发,后端解密使用openresty + lua解密,这样后端应用就不需要做任何修改了。

WebAssembly简介

WebAssembly字面意思是web的汇编语言, 是⼀种新兴的⽹⻚虚拟机标准。目前已经作为W3C规范在各大主流浏览器得到支持。

Go 语言内置了 syscall/js 包,可以在 Go 语言中直接调用 JavaScript 函数,包括对 DOM 树的操作,能用更高的性能提供更好的用户体验。

非对称加密

生成公钥和私钥, 我们这里基于openssl来生成,没有的需要安装下。

我们来生成2048位密钥对, 这个密钥对最多加密245字节文本(2048/8-11=245),

openssl genrsa -out rsa_private_key.pem 2048 
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem​​​​​​​ 


go mod init go-wasm-rsa 
GOOS=js GOARCH=wasm vim main.go

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"syscall/js"
)

func init() {
	document := js.Global().Get("document")
    // 仅支持web
	if document.IsUndefined() {
		panic("forbid")
	}
    // 我们可以在这里做一些有意思的事,例如做域名检查等
	allowedDomain := "www.mydoman.com"
	host := document.Get("location").Get("host").String()
    
    if host != allowedDomain {
        panic("forbid")
    }
}

func main() {
	c := make(chan struct{}, 0)
	js.Global().Set("enc", js.FuncOf(enc))
	<-c
}

//现实中公钥需要做一个切割,用来隐藏。这里没做处理
const pubKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1MDDhgSkSYKWBXlghk/P
NlAXxxL66zIOTdLnnyDlDcNGFMXVyjXdRPg86rr/MMhxBuzc/amGD4zEzF0xgLxA
2OrqCfHaVmr9M3Lailu1lE9piafUqfoPc65wzo+DdJmM/7xFX1FpaSqaYGsW8pyI
GDF1VGrnckVu7T/t+XEP7KBotEPAj6QCYy1DJRCq0KaThLnpgYv7Qshck/4qgVi7
Itr1KgqrRujSgdCWJy3IV5dU+ryiDr+tzPb47F7lb9cZo7uari9RfJvNwYFIYO75
AmvB90EUe43r2VysWVTPe2diOHNZXMIjGYi48M0HW31rWru7pFy5KBh7UmcZHZ5a
CQIDAQAB
-----END PUBLIC KEY-----`

func enc(this js.Value, args []js.Value) interface{} {

	authorization := []byte(args[0].String())
	block, _ := pem.Decode([]byte(pubKey))
	publicKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		panic("forbid")
	}
	//类型断言
	publicKey := publicKeyInterface.(*rsa.PublicKey)
	//对明文进行加密
	cipherText, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, authorization)
	if err != nil {
		panic("forbid")
	}
	return base64.StdEncoding.EncodeToString(cipherText)
}

Makefile

LDFLAGS='-w -s'
build:
	CGO_ENABLED=0 GOOS=js GOARCH=wasm go build -trimpath -ldflags $(LDFLAGS) -o enc.wasm

拷贝胶水代码: wasm和js的桥梁, JavaScript 支持文件,加载 wasm 文件时需要,一定要保证编译的wasm和js文件是用的同一个版本golang。 

make build

cp $(go env GOROOT)/misc/wasm/wasm_exec.js .

构建后wasm文件差不多400k, nginx需要配置gzip压缩哦,差不多100k了,很满意哦 

├── Makefile
├── go.mod
├── go.sum
├── index.html
├── main.go
└── wasm_exec.js

做一个index.html 在浏览器里测试下效果:

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Go + WebAssembly</title>
	<script src="wasm_exec.js"></script>
</head>
<body>
	<h2>待加密字符串 </h2>
	<input id="preprocess" type="text" size="200"
		value="L1co6ahggacqa$ozhbkhOXI1N5sC/l5KqSZIJu6IBoTyK0R30HWtcVPqlCy8Nm6VZK2kdwMc0eO/yIC6T2lNEX2MHbkI5MMqN6luKeIhNpBvg9EfdsqPkQZjeezJIc/7W/oakpIvtTfA1EsNBiz2Iq5t91o=" />
	<br>
	<br>
	
	<input type="button" id="btn-input" value="加密Encrypt" size="100">
	<br>
	<br>
	<h2>加密结果</h2>
	<textarea cols="80" rows="10" id="result"> </textarea>

	<script>
		(async function loadAndRunGoWasm() {
			const go = new Go();
			const response = await fetch("enc.wasm");
			const buffer = await response.arrayBuffer();
			const result = await WebAssembly.instantiate(buffer, go.importObject);
			go.run(result.instance)
		})()

		const btnInput = document.getElementById('btn-input')

		btnInput.addEventListener('click', async () => {
			const bearer = document.getElementById("preprocess").value
			const s = enc(bearer)
			document.getElementById("result").value = s
		})

	</script>
</body>
</html>
go get -u github.com/shurcooL/goexec
goexec 'http.ListenAndServe(`:8080`, http.FileServer(http.Dir(`.`)))' 

OpenResty  

编写Lua解密代码(这里之所以用lua是历史遗留问题,不让后端过多介入,对后端完全透明)

下载:lua-resty-rsa/lib/resty at master · spacewander/lua-resty-rsa · GitHub

编写dec.lua, 配置在access_by_lua_file 里

local require = require
local resty_rsa = require "resty.rsa"
local base64_decode=ngx.decode_base64

local authorization=ngx.req.get_headers()['authorization']

if authorization==nil then
    return
end

local rsa_priv_key="LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBc1Z2ZzFoTlFsdncwNTBBQWRwVUJycWJ6TkJGaHl0ZXVoWm9QTVpsZlh6a0Npbm1QCjUwb2tuVzlYU052czRYdGpxR0NsVXhJckozQmFhREZoTzJTN2tuK1N6Wm1iNXVRamxQSGhjNEJwbFRmMGVsUE8KTGRwdUxKcnZ4enVSRlJualU3ZWVvZmVjNDJHVlBad0QydDI5bU1KbkpENzZRMlBqMmxnSldBMHZHM2tkYTdwUwpXbnFDdVVYR2RadVF1VHRneG5ERXVvL1JicDV1MjRwSE82RUg1NUJQRElHY0ZJQUJkZlYyNjBWZlo4aDg2WDhQCjhLbkRlSTZVYStxMmFPaUdhaHI5b25kTHVhL1NuS1BUSVNDZjZCVzdueTRsL1Y0c1JIMm1xWXYySWNkMVF0c1YKNlJqaEh5Tm4yVzkyTlgzT0lsRHo1Tnl0VXU1THlibkhYVHhZa1FJREFRQUJBb0lCQUg3SmthYzMwNHE3N1EzTApnUWxFYUJsMG03T0RJWWVpTzg2aVhXNDFtQ280VlFxczhDU0ZxanNwbHhvc3JlQmJGdGtOamVJZXdON0d3THB2ClluVFZCQW9zVE1QUnBkT2ZENWl3ZVZ6YVZhQW9pZ3JRMGptUlJ1VjROU1VWL2hjNWxIc0tic3FXZW45S0NTZ3IKMmMyaWFxRkRoL3d2VVRUUHVka2l5anM1NFkwZkRUUGYwSEQ3MEQ0ZTB6L2NOb2dKa3RqZnZwb1dMWWlId0lnbAplcjRtUFUzQ0hVY0RtcEJHNWErN3VyVnJHOEVYMmJ1U1R6SnVabUF6d3NhSFVDUHorYU1TSHp2bTlPR3gyQlpwCjJVRDd4WGlwRmpQUHMrazQ3TlNUaHRPemxzUUNLcnBkZ1Awb3V0UC9oTDgvaktVZ0pLYWZTay9pamc3cVhOekwKTW8yakdra0NnWUVBMVQ3djhFeEFCWjNmWGZ1dUQ1Y1orMFJNTlpoMFBFVkR1NGI3V2RNV0ZNaWZIWHUxTW9zZwoybS90NEdEQWl0eUZ3Yk9xRHB2eUttMDNsTGJaZ2xLL3ZHVGVleTBFRkxmUXJPWmladk5sKzl0dEh4TStEbzdZCmhGdmdVVDNIZVBJV1ZZRytYTmdFbkNQVTVGZ1U3K3AwYWR5cjg3c1p5VkxzR0h1WWp2TjV1SjhDZ1lFQTFPc0IKb3NKRFd4NnBiTkp6bFlDdTU1N2UzdmVCRStKdXFUdWhYcXhlM2Z3MElzdUl5ZjMrRzR1UmNBdHVzWkphL08vNgpGTldRKzZqbExGaysyUjhzdWNPWjVGV1NFcG5XNENsZG9YNkVrR3RsQy8yU3A0ZWhscjRpTWRUQlhHUmZnWW9qCi9tL3JEdVIvUFdKRmQwamJqRUZXeW8xc3AzaFJidWNqcVRabDhNOENnWUFGN29WQUd1N2crUjVBZ0FLOGZraUQKdThlZTZnbTVyM2VOM05oYkRFc2Q4dUt5TUVHL0VTMnR4ZFZKRzRmZmxQakhoWmJpWnlZYVZnVm94cGxRVGJyMQpvNXlvc256ZGtxdGtVOWhDNHR4Z1lCOHQ4UndWelpWcVFTQUJRb1dzOEpiOGMrcDJySytjSkVjRXZ3cCtEZmlGCkJWVm5Kem8xWm5BWTBqOVJJcWF0SXdLQmdRQ05BVjNCOWprNVBTTWpDSFMzaTlOSlhYTm40aTIvaDNPVjdBSEEKZXhNUW5CZkMrMXdKdVlYeHBBcWJVMWJwam0xbm1WM2JNbHlqN1lSb1RHcE16RktJYTd1YzlmYVpEdnk0MDJ4SQpxVXNOZ2JJWHNNVFE0Z2ZubHQ5NmROWGhaQy9EMEVKcUhLQms2bnBCb3JVeWZETzV2UVBIZk1WNld6cEM0aHhCCjBkN05EUUtCZ1FDN3kzM0RCTkRteHZ5eEJUdksrWE5BaHVwSU5MU296STMrbUthaG1IS1QrdVplK3hZK21hdUcKb0hUMUZYTFdrTWd5eGF4SEE0SkQzTk95cDJBYTl0YVMvQjM2dVN4RjJoRFJNUmpNZnkvQVVRSG4yNUVncjdBZgpISW9iTnQyc3VVY2QrVjNtSHRIZ1ZwWlR5T1gzQzF6ZlgweTlIKzZ3TUVIbitTZ1lQOC9wTFE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ=="
local encrypted=base64_decode(authorization)

if not encrypted then
    ngx.log(ngx.ERR, "careful dec not work")
    ngx.status = ngx.HTTP_FORBIDDEN
    ngx.exit(ngx.HTTP_OK)
end

local priv, err = resty_rsa:new({ private_key = base64_decode(rsa_priv_key) })
if not priv then
   ngx.status = ngx.HTTP_UNAUTHORIZED
   ngx.exit(ngx.HTTP_OK)
   return
end
local decrypted = priv:decrypt(encrypted)
ngx.req.set_header("authorization", decrypted)

Nginx  压缩wasm

mime.types

	application/wasm                 wasm;

 nginx.conf

gzip_types ...省略其他.. application/wasm;

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐