前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >笔记:Go语言中处理字节切片时可能修改传入参数的底层切片序列的问题

笔记:Go语言中处理字节切片时可能修改传入参数的底层切片序列的问题

原创
作者头像
bowenerchen
修改2024-05-24 16:54:08
890
修改2024-05-24 16:54:08
举报
文章被收录于专栏:梵高先生梵高先生

背景

笔者在做某项功能特性开发时,需要使用对称加密算法对部分数据做加密,期间将数据以[]byte切片的形式作为入参传入时,发现在加密完成后,原始的明文会发生变化,针对这个问题笔者在 debug 过程中发现是切片与其底层切片变化引起的,于是有了这篇笔记。

先说结论

在使用 golang 语言编码时,在函数设计上,对于入参的使用需要仔细考虑,尤其在考虑使用切片slice作为入参时,需要注意对入参数据的覆盖和修改写操作:

  • 推荐传递副本:在函数参数传递之前,创建原切片的一个副本,并将其传递给函数,以避免原始数据被修改。在函数内部,避免执行任何可能修改切片的操作,如使用append或直接索引赋值。
  • 明确函数行为:在函数的文档注释中明确指出函数是否会修改切片,以及在什么情况下会进行修改。但是更推荐,在函数中使用切片时,尽可能使用只读操作,如遍历和读取,而不进行写入。
  • 返回新切片:如果需要基于输入的切片创建新的数据结构,考虑返回一个新的切片实例,而不是修改原始切片。
  • 并发安全性:在并发环境中,确保对切片的访问是线程安全的,使用锁或其他同步机制来防止竞态条件。

从一个加密函数开始

简化后的逻辑模型

代码语言:golang
复制
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 加密前的明文:

代码语言:json
复制
before EncryptWithIv plain:TURFeU16UTFOamM0T1dGelpHWm5hR3ByYkhvNU9EYzJOVFF6TWpFd1FVST0=

EncryptWithIv 加密后的明文:

代码语言:json
复制
after EncryptWithIv, plain:I2VRYPkuripgRae35ZKSD7bC/ESFrQpEZ1NnCyCRpTshzMi8Hm6J/1t8e1k=

可以看到,两段明文有了明显变化(明文本身为测试数据,无敏感信息且为了更好的展示效果,做了 base64编码处理)。

从上面的代码可以看到,在pkcs7PaddingblockMode.CryptBlocks 执行过程中,都有可能对原始数据 src 做写操作,但是什么情况下会改变 src,要解答这个问题,需要先认识[]byte切片的几个特性。

切片和底层切片的关系

在Go语言中,切片(slice)是一种基于数组的数据结构,它提供了一种动态调整大小的能力,使得数据的存储和管理更加灵活。

切片的内部结构在src/runtime/slice.go中定义,它包含三个主要部分:指向底层切片的指针、切片的长度以及切片的容量。

通过 make 创建切片

比如通过 make 分配一个长度为 1024 字节的切片。

代码语言:golang
复制
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)
}
切片的三个属性
切片的三个属性

切片属性

代码语言:golang
复制
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])
}
切片长度、容量及起始地址
切片长度、容量及起始地址

底层切片序列的重新分配

代码语言:golang
复制
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)
}
底层切片会根据长度来做调整
底层切片会根据长度来做调整

切片长度变化不一定导致底层切片变化

代码语言:golang
复制
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 的长度,那么就会生成一个新的底层切片:

代码语言:golang
复制
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)
}
填充后生成新的底层切片,原切片不受影响
填充后生成新的底层切片,原切片不受影响

基于原底层切片进行数据写入

代码语言:golang
复制
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 参数,在做写操作前最好做一份冗余拷贝,以避免对原始数据的写操作。

代码语言:golang
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 先说结论
  • 从一个加密函数开始
    • 简化后的逻辑模型
      • 切片和底层切片的关系
        • 通过 make 创建切片
        • 切片属性
        • 底层切片序列的重新分配
        • 切片长度变化不一定导致底层切片变化
    • 加密填充引起的切片值变化
      • 生成新的底层切片
        • 基于原底层切片进行数据写入
        • 问题的关键
        • 一种可能的降低风险的实现方式
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档