是否可以"安全地"将Golang字符串的内存归零?

Jak*_*cas 7 memory security string go cgo

最近我一直libsodium在使用cgo 设置我的一个项目,以便使用crypto_pwhash_strcrypto_pwhash_str_verify功能.

这一切都非常顺利,我现在有一小部分函数,​​它们[]byte以纯文本密码的形式接收并散列它,或者将它与另一个[]byte进行比较以验证它.

我使用的一个原因[]byte,而不是string因为,从我迄今约去学,我可以过明文密码至少循环和零所有字节,甚至将指针传递到libsodiumsodium_memzero功能,为了不让它在内存中停留超过它需要的时间.

这对于我能够直接以字节读取输入的应用程序来说很好,但我现在正尝试在一个小型Web应用程序中使用它,我需要使用该POST方法从表单中读取密码.

从我在Go源代码和文档中看到的内容,r.ParseForm在请求处理程序中使用将所有表单值解析mapstrings.

问题是因为stringGo中的s是不可变的,我不认为我可以做任何关于POST将表单中编写的密码的内存归零的事情; 至少,只使用Go.

因此,似乎我唯一的(简单)选项是将unsafe.Pointer一个函数传递给C中的函数以及字节数,让C为我归零内存(例如,将其传递给上述sodium_memzero函数).

我已经尝试了这一点,并且毫不奇怪它当然有效,但是我string在Go中留下了一个不安全的东西,如果在一个函数中使用它fmt.Println会使程序崩溃.

我的问题如下:

  • 我是否应该接受密码将被POST编辑并解析为字符串,我不应该把它搞乱,只是等待GC启动?(不理想)
  • 是否将string使用cgo ok 的内存清零,前提是代码中明显记录了字符串变量不应该再次使用?
  • string使用cgo 的内存清零会不会像崩溃一样?
  • 是否值得为它编写一种装饰器来http.Request添加一个直接解析表单值的函数,[]byte因此我可以在它们到达时完全控制值?

编辑:为了澄清,网络应用程序和表单POST只是一个简单的例子,我可能只是使用Go的标准库以一种形式传递敏感数据string.我更感兴趣的是我的所有问题是否可能/值得在某些情况下尽可能快地清理内存中的数据更多是一个安全问题.

Jef*_*erg 6

如果您想接受具有多字节字符的密码,我认为您的方案一般不会起作用.

处理具有多字节字符的密码要求您首先对它们进行标准化(有多个不同的字节序列可能是"Å"之类的,并且您输入的内容会因键盘,操作系统以及月亮的相位而异.

因此,除非您想要重写所有Go的Unicode规范化代码以处理字节数组,否则您将遇到问题.

鉴于在这个问题上似乎没有太多活动,我将假设大多数人之前不需要/想要研究这个问题,或者认为它不值得花时间.

实际上,直到今天我才注意到这个问题.相信我,我已经考虑过了.


Jak*_*cas 5

鉴于在这个问题上似乎没有太多活动,我将假设大多数人之前不需要/想要研究这个问题,或者认为它不值得花时间.因此,尽管我对Go的内部运作方式一无所知,但我会发布自己的发现作为答案.

我应该在这个答案前加上一个免责声明,因为Go是垃圾收集语言而我不知道它在内部是如何工作的,以下信息可能实际上并不能保证任何内存实际上都被清除为零,但这不会阻止我从尝试; 毕竟,在我看来,内存中的纯文本密码越少越好.

考虑到这一点,这就是我发现的所有工作(据我所知)和libsodium; 到目前为止,至少没有任何一个程序崩溃过.

首先,正如你可能已经知道string在号S是不可变的,所以在技术上它们的价值不应该被改变,但是如果我们使用unsafe.Pointerstring通过CGO在Go或C,我们实际上可以覆盖存储在数据string价值; 我们无法保证在内存中的任何其他地方都没有任何其他数据副本.

出于这个原因,我让我的密码相关函数[]byte专门处理变量,以减少在内存中复制的可能的纯文本密码的数量.

我还返回[]byte传递给所有密码函数的纯文本密码的引用,因为将a转换string为a []byte将分配新内存并复制内容.这样,至少如果你将你转换string[]byte就地而不首先将它分配给一个变量,你仍然可以[]byte在函数调用完成后访问new ,并将该内存归零.

以下是我想出的要点.您可以填写空白,包括libsodiumC库并编译它以自己查看结果.

对我来说,它在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]
Run Code Online (Sandbox Code Playgroud)

然后在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
Run Code Online (Sandbox Code Playgroud)

所以它看起来像一个成功的,但因为我们不能保证不存在明文密码的副本存储在其他地方我认为是尽可能我们可以用它.

下面的代码只是将unsafe.Pointer带有bytes 数的s传递给sodium_memzeroC中的函数来实现这一点.所以内存的实际归零是留给你的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)
    }
}
Run Code Online (Sandbox Code Playgroud)

然后全部使用它:

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))
}
Run Code Online (Sandbox Code Playgroud)