笔者在做某项功能特性开发时,需要使用对称加密算法对部分数据做加密,期间将数据以[]byte
切片的形式作为入参传入时,发现在加密完成后,原始的明文会发生变化,针对这个问题笔者在 debug 过程中发现是切片与其底层切片变化引起的,于是有了这篇笔记。
在使用 golang 语言编码时,在函数设计上,对于入参的使用需要仔细考虑,尤其在考虑使用切片slice
作为入参时,需要注意对入参数据的覆盖和修改写操作:
package main
import (
"bytes"
"crypto/aes"
cipher2 "crypto/cipher"
"errors"
"fmt"
)
func EncryptWithIv(key, iv []byte, src []byte) (cipher []byte, err error) {
if len(src) == 0 {
return nil, errors.New("source is empty")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode := cipher2.NewCBCEncrypter(block, iv)
fmt.Println("before padding", &src[0], len(src), cap(src), src)
src = pkcs7Padding(src, block.BlockSize())
fmt.Println("after padding", &src[0], len(src), cap(src), src)
blockMode.CryptBlocks(src, src)
fmt.Println("after encrypt", &src[0], len(src), cap(src), src)
return src, nil
}
func pkcs7Padding(originData []byte, blockSize int) []byte {
padding := blockSize - len(originData)%blockSize
pad := bytes.Repeat([]byte{byte(padding)}, padding)
return append(originData, pad...)
}
如上代码所示,我尝试使用 EncryptWithIv 对数据 src 进行加密,但是加密完成后,在对明文做校验比较时,发现明文发生了变化,在业务的 debug 日志中有类似于下面这样的记录:
EncryptWithIv
加密前的明文:
before EncryptWithIv plain:TURFeU16UTFOamM0T1dGelpHWm5hR3ByYkhvNU9EYzJOVFF6TWpFd1FVST0=
EncryptWithIv
加密后的明文:
after EncryptWithIv, plain:I2VRYPkuripgRae35ZKSD7bC/ESFrQpEZ1NnCyCRpTshzMi8Hm6J/1t8e1k=
可以看到,两段明文有了明显变化(明文本身为测试数据,无敏感信息且为了更好的展示效果,做了 base64编码处理)。
从上面的代码可以看到,在pkcs7Padding
和 blockMode.CryptBlocks
执行过程中,都有可能对原始数据 src 做写操作,但是什么情况下会改变 src,要解答这个问题,需要先认识[]byte
切片的几个特性。
在Go语言中,切片(slice)是一种基于数组的数据结构,它提供了一种动态调整大小的能力,使得数据的存储和管理更加灵活。
切片的内部结构在src/runtime/slice.go中定义,它包含三个主要部分:指向底层切片的指针、切片的长度以及切片的容量。
比如通过 make
分配一个长度为 1024 字节的切片。
func TestSliceCap(t *testing.T) {
// 切片的长度、容量、起始地址
size := 1024
array_1 := make([]byte, size)
len_1 := len(array_1)
cap_1 := cap(array_1)
fmt.Println(len_1, cap_1, &array_1[0])
assert.True(t, len_1 == size)
assert.True(t, cap_1 >= size)
}
func TestSliceCap2(t *testing.T) {
size := 1024
array_1 := make([]byte, size)
len_1 := len(array_1)
cap_1 := cap(array_1)
fmt.Println(len_1, cap_1, &array_1[0])
assert.True(t, len_1 == size)
assert.True(t, cap_1 >= size)
// 切片的长度、底层切片容量
array_2 := array_1[:16]
len_2 := len(array_2)
cap_2 := cap(array_2)
fmt.Println(len_2, cap_2, &array_2[0])
assert.True(t, len_2 == 16)
assert.True(t, cap_2 >= len_2)
assert.True(t, &array_1[0] == &array_2[0])
}
func TestSliceCap3(t *testing.T) {
size := 4
array_1 := make([]byte, size)
len_1 := len(array_1)
cap_1 := cap(array_1)
fmt.Println(len_1, cap_1, &array_1[0])
assert.True(t, len_1 == size)
assert.True(t, cap_1 >= size)
ptr1 := &array_1[0]
// 当容量长度不够不够时分配新的底层切片
array_2 := array_1[:2]
data := bytes.Repeat([]byte("a"), 10)
array_2 = append(array_2, data...)
len_2 := len(array_2)
cap_2 := cap(array_2)
ptr2 := &array_2[0]
fmt.Println(len_2, cap_2, &array_2[0])
assert.False(t, ptr1 == ptr2)
}
func TestSliceCap5(t *testing.T) {
size := 1024
array_1 := make([]byte, size)
len_1 := len(array_1)
cap_1 := cap(array_1)
ptr_1 := &array_1[0]
fmt.Println(len_1, cap_1, &array_1[0])
assert.True(t, len_1 == size)
assert.True(t, cap_1 >= size)
// 底层切片不变,切片长度比底层切片小
array_2 := array_1[:15]
len_2 := len(array_2)
cap_2 := cap(array_2)
ptr_2 := &array_2[0]
fmt.Println(len_2, cap_2, &array_2[0])
// 切片修改数据或者增加数据
data := bytes.Repeat([]byte("a"), 1)
array_2 = append(array_2, data...)
ptr_3 := &array_2[0]
len_3 := len(array_2)
cap_3 := cap(array_2)
fmt.Println(len_3, cap_3, &array_2[0])
// 底层切片没有被重新分配,因为空间足够
assert.True(t, ptr_3 == ptr_2)
assert.True(t, ptr_3 == ptr_1)
assert.True(t, ptr_2 == ptr_1)
}
在做对称加密如 AES-CBC 加密时,明文数据长度需要做填充,以满足明文长度为 AES BLOCK 的整数倍的要求。
当传入的 src 切片数据长度需要填充时,如果其长度超过底层 cap 的长度,那么就会生成一个新的底层切片:
func TestEncrypt1(t *testing.T) {
key := make([]byte, 32)
iv := make([]byte, 16)
// cap 长度为 15,需要分配新的底层切片
src := make([]byte, 15)
// 在新的底层切片上操作
_, err := EncryptWithIv(key, iv, src)
assert.Nil(t, err)
fmt.Println("original src:",&src[0], src)
}
func TestEncrypt2(t *testing.T) {
key := make([]byte, 32)
iv := make([]byte, 16)
// cap 长度为 32,不需要分配新的底层切片
src := make([]byte, 32)
needPadded := src[:15]
_, err := EncryptWithIv(key, iv, needPadded)
assert.Nil(t, err)
fmt.Println("original src:", &src[0], src)
}
切片(slice)是一种引用类型数据,当切片作为参数传递给函数时,实际上是传递了对原切片的引用,而不是切片的副本。
这意味着,如果函数内部对切片进行了修改,这些修改也会影响到原切片。
对于传入的 src 参数,在做写操作前最好做一份冗余拷贝,以避免对原始数据的写操作。
func EncryptWithIv2(key, iv []byte, src []byte) (cipher []byte, err error) {
if len(src) == 0 {
return nil, errors.New("source is empty")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode := cipher2.NewCBCEncrypter(block, iv)
fmt.Println("before padding", &src[0], len(src), cap(src), src)
// 拷贝原始数据
srcCopy := make([]byte, len(src))
copy(srcCopy, src)
// 对拷贝数据做填充操作
srcCopy = pkcs7Padding(srcCopy, block.BlockSize())
fmt.Println("after padding", &src[0], len(src), cap(src), src)
// 对拷贝数据做加密写操作
blockMode.CryptBlocks(srcCopy, srcCopy)
fmt.Println("after encrypt", &src[0], len(src), cap(src), src)
return srcCopy, nil
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。