最近我使用cgo在我的一个项目中设置了libsodium
,以便使用crypto_pwhash_str
和crypto_pwhash_str_verify
函数。
这一切都非常顺利,我现在有一小部分函数,它们以纯文本密码的形式接收[]byte
并哈希,或者将它与另一个[]byte
进行比较以验证它。
我使用[]byte
而不是string
的原因是因为,从我迄今为止学到的关于Go的内容来看,我至少可以循环使用纯文本密码并将所有字节归零,甚至可以将指针传递给libsodium
的sodium_memzero
功能,为了不让它在内存中停留超过它需要的时间。
这对于我能够直接以字节读取输入的应用程序来说很好,但我现在正尝试在小型Web应用程序中使用它,我需要使用POST
方法从表单中读取密码。
从我在Go源代码和文档中看到的内容,在请求处理程序中使用r.ParseForm
将所有表单值解析为map
的string
。
问题在于,因为Go中的string
s是不可变的,所以我认为我无法做任何关于将密码的内存归零的形式; POST
ed in form;至少,只使用Go。
因此,似乎我唯一的(简单)选项是将unsafe.Pointer
传递给C中的函数以及字节数,让C为我归零内存(例如,将其传递给前面提到的sodium_memzero
函数)。
我已经尝试了这一点,并且毫不奇怪它当然有效,但后来我在Go中留下了一个不安全的string
,如果在像fmt.Println
这样的函数中使用会使程序崩溃。
我的问题如下:
POST
ed并解析为字符串,我不应该把它搞乱,只是等待GC启动? (不理想)string
的内存归零,前提是代码中明显记录了字符串变量不应再次使用?string
的内存归零会做类似崩溃的事吗?http.Request
编写一种装饰器,它添加了一个函数来直接将表单值解析为[]byte
,因此当它们到达时我可以完全控制值?编辑:为了澄清,网络应用程序和表单POST
只是一个简单的例子,我可能只是从使用Go的标准库以string
的形式传递敏感数据。我更感兴趣的是我的所有问题是否可能/值得在某些情况下尽可能快地清理内存中的数据更多是一个安全问题。
如果您想接受具有多字节字符的密码,我认为您的方案一般不会起作用。
处理具有多字节字符的密码要求您首先对它们进行标准化(有多个不同的字节序列可能是“Å”之类的,并且您输入的内容会因键盘,操作系统以及月亮的相位而异。
因此,除非您想要重写所有Go的Unicode规范化代码以处理字节数组,否则您将遇到问题。
鉴于在这个问题上似乎没有太多活动,我将假设大多数人之前不需要/想要研究这个问题,或者认为它不值得花时间。
实际上,直到今天我才注意到这个问题。相信我,我已经考虑过了。
鉴于在这个问题上似乎没有太多活动,我将假设大多数人之前不需要/想要研究这个问题,或者认为它不值得花时间。因此,尽管我对Go的内部运作方式一无所知,但我会发布自己的发现作为答案。
我应该在这个答案前面加上一个免责声明,因为Go是垃圾收集语言,我不知道它是如何在内部工作的,以下信息可能实际上并不能保证任何内存实际上都被清除为零,但这不会阻止我从尝试;毕竟,在我看来,内存中的纯文本密码越少越好。
考虑到这一点,这是我发现的所有工作(据我所知)与libsodium
一起工作;到目前为止,至少没有任何一个程序崩溃过。
首先,你可能已经知道Go中的string
s是不可变的,所以从技术上来说它们的值不应该改变,但是如果我们在Go中使用unsafe.Pointer
或者通过Cgo在C中使用string
,我们实际上可以覆盖存储的数据string
值;我们无法保证在内存中的任何其他地方都没有任何其他数据副本。
出于这个原因,我使用密码相关的函数专门处理[]byte
变量,以减少在内存中复制的可能的纯文本密码的数量。
我还返回[]byte
引用的纯文本密码,该密码被传递到所有密码函数,因为将string
转换为[]byte
将分配新内存并复制内容。这样,至少如果你将string
原地转换为[]byte
而不将其分配给变量,你仍然可以在函数调用完成后访问新的[]byte
并将该内存归零。
以下是我想出的要点。您可以填写空白,包括libsodium
C库并编译它以自己查看结果。
对我来说,它在调用MemZero*
函数之前输出:
pwd : Correct Horse Battery Staple
pwdBytes: [67 111 114 114 101 99 116 32 72 111 114 115 101 32 66 97 116 116 101 114 121 32 83 116 97 112 108 101]
然后在MemZero*
函数之后调用:
pwd :
pwdBytes: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Hash: $argon2i$v=19$m=131072,t=6,p=1$N05osI8nuTjftzfAYBIcbA$3yb92yt9S9dRmPtlSV/J8jY4DG3reqm+2eV+fi54Its
所以它看起来很成功,但由于我们不能保证在内存中的其他位置没有纯文本密码的副本,我认为这是我们可以使用它。
下面的代码只是将带有unsafe.Pointer
s数量的byte
传递给C中的sodium_memzero
函数来实现这一目的。所以内存的实际归零是由libsodium
决定的。
如果我在代码中留下任何错误或任何不起作用的东西,我道歉,但我不想粘贴太多,只有相关部分。
例如,如果你真的需要,你也可以使用像mlock
这样的函数,但由于这个问题集中在将string
归零,我将在这里展示。
package sodium
// Various imports, other functions and <sodium.h> here...
func init() {
if err := sodium.Init(); err != nil {
log.Fatalf("sodium: %s", err)
}
}
func PasswordHash(pwd []byte, opslimit, memlimit int) ([]byte, []byte, error) {
pwdPtr := unsafe.Pointer(&pwd[0])
hashPtr := unsafe.Pointer(&make([]byte, C.crypto_pwhash_STRBYTES)[0])
res := C.crypto_pwhash_str(
(*C.char)(hashPtr),
(*C.char)(pwdPtr),
C.ulonglong(len(pwd)),
C.ulonglong(opslimit),
C.size_t(memlimit),
)
if res != 0 {
return nil, pwd, fmt.Errorf("sodium: passwordhash: out of memory")
}
return C.GoBytes(hashPtr, C.crypto_pwhash_STRBYTES), pwd, nil
}
func MemZero(p unsafe.Pointer, size int) {
if p != nil && size > 0 {
C.sodium_memzero(p, C.size_t(size))
}
}
func MemZeroBytes(bytes []byte) {
if size := len(bytes); size > 0 {
MemZero(unsafe.Pointer(&bytes[0]), size)
}
}
func MemZeroStr(str *string) {
if size := len(*str); size > 0 {
MemZero(unsafe.Pointer(str), size)
}
}
然后全部使用它:
package main
// Imports etc here...
func main() {
// Unfortunately there is no guarantee that this won't be
// stored elsewhere in memory, but we will try to remove it anyway
pwd := "Correct Horse Battery Staple"
// I convert the pwd string to a []byte in place here
// Because of this I have no reference to the new memory, with yet
// another copy of the plain password hanging around
// The function always returns the new []byte as the second value
// though, so we can still zero it anyway
hash, pwdBytes, err := sodium.PasswordHash([]byte(pwd), 6, 134217728)
// Byte slice and string before MemZero* functions
fmt.Println("pwd :", pwd)
fmt.Println("pwdBytes:", pwdBytes)
// No need to keep a plain-text password in memory any longer than required
sodium.MemZeroStr(&pwd)
sodium.MemZeroBytes(pwdBytes)
if err != nil {
log.Fatal(err)
}
// Byte slice and string after MemZero* functions
fmt.Println("pwd :", pwd)
fmt.Println("pwdBytes:", pwdBytes)
// We've done our best to make sure we only have the hash in memory now
fmt.Println("Hash:", string(hash))
}
在Go中处理安全值比使用C或C ++更难。那是因为GC,复制和乱搞任何记忆的感觉。
所以,第一步是获得一些GC不能搞砸的内存。为此,我们要么根据需要调整cgo和malloc;或者使用像mmap和VirtualAlloc这样的系统调用;然后像往常一样绕过生成的切片。
下一步是告诉操作系统你不希望这个内存被换成磁盘,所以你要mlock或VirtualLock它。
在退出之前,使用libsodium或通过简单地迭代切片将切片归零,将每个元素设置为零。使用字符串是不可能的,我不确定我是否建议手动擦除字符串的内存。我的意思是,我不能立即发现它有什么问题,但是...它感觉不对。无论如何,没有人使用字符串作为安全值。
有一个专门用于存储安全值的库(我的),它完成了我上面描述的以及其他一些事情。您可能会发现它很有用:https://github.com/awnumar/memguard
“无论如何,没有人使用字符串作为安全值。”
除了KDF中用于解锁密文或直接解密的密码。
如果您尝试改变字符串的底层缓冲区,则字符串分配中使用的内存会触发分段错误:
https://medium.com/kokster/mutable-strings-in-golang-298d422d01bc
与memguard不可变缓冲区相同。
我已经尝试在给定的地址上使用unix.Mprotect但我认为技巧是我必须找到存储字符串缓冲区的实际内存页地址,而不是指向缓冲区起点的指针,以便有效地执行此操作。
暂时找到合适的解决方案对我来说有点太多的工作,但是知道字符串是不可变的并且从这里堆积到副本中的副本存在于记忆中,我认为如果你使用memguard并且有处理密码,在第一时间将密码放入memguard缓冲区,之后只处理其中的数据。
正是出于这样的原因,Qubes被设计出来,才能在应用程序之间建立更强大的界限。如果您的程序装在VM容器内,它根本无法到达该框之外。如果你的程序运行恶意代码,那么只有攻击向量。
由于网络数据包以[]字节到达,因此可以根据需要将其中敏感的任何内容清零。由于键盘输入端由OS控制,因此只需找到(或可能写入)直接转到可变字节切片的控制台文本输入函数,然后应用顶部引用的语句。
考虑到这一点,我现在正在改变我的代码,以便在使用它之后不需要在任何地方使用字符串变量。