WASM 中 AES 加解密实现

最近对项目所有的 HTTP 请求响应进行了 AES 加解密改造,但在 JavaScript 中保管 AES Key 有着非常大的安全风险,将加解密逻辑交由难以反编译的 WebAssembly 实现,则是一种比较安全的选择。

以下是 Go 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package main

import (
"bytes"
"encoding/base64"
"encoding/hex"
"errors"
"syscall/js"
"crypto/cipher"
"crypto/aes"
)

const aesDefaultKeyHex = "d86d7bab3d6fc0aab9dccad97652f2d2"

// AES 加密函数
func aesEncrypt(data string, aesKeyHex string) (string, error) {
aesKey, err := hex.DecodeString(aesKeyHex)
if err != nil {
return "", err
}
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", err
}

// 填充数据
dataBytes := []byte(data)
blockSize := block.BlockSize()
paddedData := pad(dataBytes, blockSize)

// ECB 加密
ciphertext := make([]byte, len(paddedData))
mode := newECB(block)
mode.Encrypt(ciphertext, paddedData)

return base64.StdEncoding.EncodeToString(ciphertext), nil
}

// AES 解密函数
func aesDecrypt(encryptedData string, aesKeyHex string) (string, error) {
aesKey, err := hex.DecodeString(aesKeyHex)
if err != nil {
return "", err
}
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", err
}

// 解码 base64 的密文
ciphertext, err := base64.StdEncoding.DecodeString(encryptedData)
if err != nil {
return "", err
}

// 检查密文长度是否为块大小的倍数
if len(ciphertext)%block.BlockSize() != 0 {
return "", errors.New("密文长度不是块大小的倍数")
}

// ECB 解密
decrypted := make([]byte, len(ciphertext))
mode := newECB(block)
mode.Decrypt(decrypted, ciphertext)

// 去填充
unpaddedData, err := unpad(decrypted)
if err != nil {
return "", err
}

return string(unpaddedData), nil
}

// 填充数据
func pad(data []byte, blockSize int) []byte {
pad := blockSize - len(data)%blockSize
padding := bytes.Repeat([]byte{byte(pad)}, pad)
return append(data, padding...)
}

// 去填充数据
func unpad(data []byte) ([]byte, error) {
length := len(data)
if length == 0 {
return nil, errors.New("数据长度为 0")
}
pad := data[length-1]
if pad > byte(length) {
return nil, errors.New("填充无效")
}
return data[:length-int(pad)], nil
}

// ECB 模式实现
type ecb struct {
b cipher.Block
}

// 创建 ECB 模式实例
func newECB(b cipher.Block) *ecb {
return &ecb{b: b}
}

// ECB 加密函数
func (e *ecb) Encrypt(dst, src []byte) {
if len(src)%e.b.BlockSize() != 0 {
panic("输入数据大小必须为块大小的整数倍")
}
for len(src) > 0 {
e.b.Encrypt(dst, src[:e.b.BlockSize()])
src = src[e.b.BlockSize():]
dst = dst[e.b.BlockSize():]
}
}

// ECB 解密函数
func (e *ecb) Decrypt(dst, src []byte) {
if len(src)%e.b.BlockSize() != 0 {
panic("输入数据大小必须为块大小的整数倍")
}
for len(src) > 0 {
e.b.Decrypt(dst, src[:e.b.BlockSize()])
src = src[e.b.BlockSize():]
dst = dst[e.b.BlockSize():]
}
}

func main() {
// 暴露 AES 加密函数到 JavaScript
js.Global().Set("aesEncrypt", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
data := p[0].String()
key := aesDefaultKeyHex
encrypted, err := aesEncrypt(data, key)
if err != nil {
return js.ValueOf(err.Error())
}
return js.ValueOf(encrypted)
}))

// 暴露 AES 解密函数到 JavaScript
js.Global().Set("aesDecrypt", js.FuncOf(func(this js.Value, p []js.Value) interface{} {
encryptedData := p[0].String()
key := aesDefaultKeyHex
decrypted, err := aesDecrypt(encryptedData, key)
if err != nil {
return js.ValueOf(err.Error())
}
return js.ValueOf(decrypted)
}))

<-make(chan struct{}) // 防止程序退出
}

将上述 Go 代码编译为 WebAssembly,生成 aes.wasm 文件:

1
$env:GOOS="js"; $env:GOARCH="wasm"; go build -o aes.wasm aes.go

在 JavaScript 中调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script src="wasm_exec.js"></script>

<input type="text" id="inputText" />
<button id="encryptBtn">加密</button>
<p>Encrypted: <span id="encryptedText"></span></p>
<input type="text" id="inputEncryptedText" />
<button id="decryptBtn">解密</button>
<p>Decrypted: <span id="decryptedText"></span></p>

<script>
const go = new Go()
WebAssembly.instantiateStreaming(fetch('aes.wasm'), go.importObject).then(
(result) => {
go.run(result.instance)
}
)

document.getElementById('encryptBtn').addEventListener('click', () => {
const inputText = document.getElementById('inputText').value
const encrypted = aesEncrypt(inputText)
document.getElementById('encryptedText').innerText = encrypted
})

document.getElementById('decryptBtn').addEventListener('click', () => {
const inputEncryptedText = document.getElementById('inputEncryptedText').value
const decrypted = aesDecrypt(inputEncryptedText)
document.getElementById('decryptedText').innerText = decrypted
})
</script>