doubu4406 2016-10-10 23:07
浏览 107
已采纳

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

Recently I've been setting up libsodium in one of my projects by using cgo, in order to use the crypto_pwhash_str and crypto_pwhash_str_verify functions.

This has all gone very smoothly and I now have a small collection of functions that receive a []byte in the form of a plain-text password and either hash it, or compare it against another []byte to verify it.

My reason for using a []byte instead of a string is because, from what I've learnt so far about Go, I can at least loop over the plain-text password and zero all of the bytes, or even pass a pointer into libsodium's sodium_memzero function, in order to not leave it hanging around in memory longer than it needs to.

This is fine for applications where I have the ability to read input directly as bytes, but I'm now trying to use it in a small web application where I need to read passwords from a form using the POST method.

From what I can see in the Go source code and documentation, using r.ParseForm in a request handler will parse all of the form values into a map of strings.

The problem is that because strings in Go are immutable I don't think I can do anything about zeroing the memory of a password that was POSTed in the form; at least, using only Go.

So it seems like my only (easy) option would be to pass an unsafe.Pointer into a function in C along with the number of bytes and let C zero the memory for me instead (for example, passing it to the aforementioned sodium_memzero function).

I have tried this, and unsurprisingly it does of course work, but then I'm left with an unsafe string in Go, which, if used in a function like fmt.Println will crash the program.

My questions are as follows:

  • Should I just accept that passwords will be POSTed and parsed as strings and that I shouldn't mess with it and just wait for the GC to kick in? (not ideal)
  • Is zeroing the memory of a string using cgo ok, provided it's obviously documented in the code that the string variable should not be used again?
  • Will zeroing the memory of a string using cgo ever do something like crashing the GC?
  • Is it worth writing a sort of decorator for http.Request that adds a function to parse form values directly as []byte so I have complete control over the values when they arrive?

Edit: To clarify, the web app and form POST is just a convenient example of a case where I might be handed sensitive data just from using Go's standard library in the form of a string. I'm more just interested in whether all of my questions are possible/worthwhile in some case were cleaning up data in memory as quickly as possible was more of a security concern.

  • 写回答

4条回答 默认 最新

  • douzhuo6931 2016-10-12 11:15
    关注

    Given that there doesn't seem to be much activity on this question, I'm going to just assume that most people haven't needed/wanted to look into this before, or haven't thought it was worth the time. As such I will just post my own findings as an answer despite my ignorance regarding the inner-workings of Go.

    I should preface this answer with a disclaimer that since Go is a Garbage Collected language and I do not know how it works internally the following information may not actually guarantee any memory to actually be cleared to zero at all, but that won't stop me from trying; after all, the fewer plain-text passwords in memory the better, in my opinion.

    With that in mind this is everything I have found to work (as far as I can tell) in conjunction with libsodium; so far none of it has crashed any of my programs at least.

    First of all, as you probably already know strings in Go are immutable, so technically their value shouldn't be changed, but if we use an unsafe.Pointer to the string in Go or in C via Cgo, we can actually overwrite the data stored in the string value; we just can't guarantee there aren't any other copies of the data anywhere else in memory.

    For this reason I made my password related functions deal with []byte variables exclusively to cut down on the number of possible plain-text passwords being copied around memory.

    I also return the []byte reference for the plain text password that gets passed into all password functions, since converting a string into a []byte will allocate new memory and copy the contents over. This way, at least if you convert your string to a []byte in-place without assigning it to a variable first you can still get access to the new []byte after the function call has finished and zero that memory as well.

    Below is the gist of what I came up with. You can fill in the blanks, include the libsodium C library and compile it to see the results for yourself.

    For me it output this before the MemZero* function were called:

    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]
    

    Then this after the MemZero* function were called:

    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
    

    So it looks like a success, but since we can't guarantee there are no copies of the plain-text password elsewhere in memory I think that is as far as we can go with it.

    The code below simply passes an unsafe.Pointer with the number of bytes to the sodium_memzero function in C to achieve this. So the actual zeroing of memory is left up to libsodium.

    I apologise if I left any typos or anything in the code that doesn't work, but I didn't want to paste in too much, only the relevant parts.

    For example, you could also employ the use of functions like mlock if you really needed to, but since this question was focused on zeroing a string I will just show that here.

    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)
        }
    }
    

    And then to use it all:

    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))
    }
    
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(3条)

报告相同问题?

悬赏问题

  • ¥15 随身WiFi网络灯亮但是没有网络,如何解决?
  • ¥15 gdf格式的脑电数据如何处理matlab
  • ¥20 重新写的代码替换了之后运行hbuliderx就这样了
  • ¥100 监控抖音用户作品更新可以微信公众号提醒
  • ¥15 UE5 如何可以不渲染HDRIBackdrop背景
  • ¥70 2048小游戏毕设项目
  • ¥20 mysql架构,按照姓名分表
  • ¥15 MATLAB实现区间[a,b]上的Gauss-Legendre积分
  • ¥15 delphi webbrowser组件网页下拉菜单自动选择问题
  • ¥15 linux驱动,linux应用,多线程