2016-10-10 163 views
2

最近我使用cgo在我的一個項目中設置了libsodium,以便使用crypto_pwhash_strcrypto_pwhash_str_verify函數。是否可以「安全地」清零Golang字符串的內存?

這一切都非常順利,我現在有一個函數的小集合,它以明文密碼的形式接收[]byte並散列或與其他[]byte進行比較以驗證它。

我使用一個[]byte代替string的原因是因爲,從我迄今約去學,我可以過明文密碼至少循環和零所有字節,甚至傳遞指向libsodiumsodium_memzero函數,以便不讓它在內存中的時間長於需要的時間。

這對於我能夠直接以字節讀取輸入的應用程序來說很好,但我現在試圖在小型Web應用程序中使用它,我需要使用POST方法從窗體讀取密碼。

從我在Go源代碼和文檔中看到的,在請求處理程序中使用r.ParseForm將將所有表單值解析爲mapstring s。

問題是,因爲Go中的string是不可變的,所以我不認爲我可以做任何關於在表單中對POST ed的密碼進行歸零的操作;至少,只使用Go。

所以看起來我唯一的(簡單)選項是將一個unsafe.Pointer與字節數一起傳遞給C中的一個函數,並讓C爲我替換內存(例如,將它傳遞給前面提到的sodium_memzero功能)。

我已經嘗試了這一點,毫不奇怪,它當然可以工作,但後來我在Go中留下了一個不安全的string,如果在fmt.Println之類的函數中使用該函數,將會使程序崩潰。

我的問題如下:

  • 我應該接受的密碼將POST ED和解析爲字符串,我不應該惹它,只是等待GC在踢? (不理想)
  • 使用cgo ok清零string的內存,只要它明顯記錄在代碼中,字符串變量不應該再次使用?
  • 使用cgo將string的內存置零將執行類似崩潰GC的操作?
  • 是否值得爲http.Request寫一種修飾器,它增加了一個函數來直接解析表格值爲[]byte,所以當他們到達時我完全控制了這些值。

編輯:爲了澄清,Web應用程序和形式POST是我可能只是使用Go的標準庫中string的形式交給敏感數據的情況下的只是一個方便的例子。我更感興趣的是,在某些情況下,儘可能快地清理內存中的數據更多是出於安全考慮,我的所有問題是否可能/值得。

+0

當你說調零內存你的意思是調用realloc並給它一個0的大小?因爲如果你的意思是把塊中的所有位設置爲0,我不確定這個點是什麼。我的直覺說你應該依靠GC,我認爲你可以安排你的代碼,使地圖很快地離開作用域(比如將字符串傳入你在goroutine中調用的函數並移動一個函數),所以我沒有看到內存被認爲是真正的問題。我的意思是,這裏的記憶關注是什麼?密碼不是很重要。 – evanmcdonnal

+1

如果一個密碼通過一個HTTP POST進入,那麼將一個特定的字符串置零是沒有太大意義的,因爲你沒有直接從線上讀取它,也不知道在內存中有多少個副本。 – JimB

+0

@JimB這是非常真實的,也許我應該更好地表達我的問題,因爲我似乎已經給出了我想在我的Web應用程序中執行此操作的印象。真的,這個web應用程序的例子只是我關於這個主題的想法的觸發器,而我更關心的是,將Go的字符串內存調零實際上是否可行/「安全」足以在Go中執行,需要。 –

回答

2

鑑於在這個問題上似乎沒有太多的活動,我會假設大多數人沒有必要/想要以前看過,或者沒有想過這是值得的時間。因此,儘管我對Go的內在運作一無所知,我仍然會將自己的發現作爲答案發布。

我應該在這個答案的前面加一個免責聲明,因爲Go是一個垃圾收集語言,我不知道它是如何在內部工作的,下面的信息實際上可能並不保證任何內存實際上被清零爲零,不要阻止我嘗試;畢竟,在我看來,內存中的純文本密碼越少越好。

考慮到這一點,這是我發現的所有工作(據我所知)與libsodium;至少到目前爲止,它還沒有讓我的任何程序崩潰。

首先,正如你可能已經在圍棋知道string s爲不可變的,所以在技術上它們的價值不應該被改變,但是如果我們通過CGO使用unsafe.Pointer在圍棋中string或C,我們實際上可以覆蓋存儲在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.Pointerbyte的數量傳遞給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)) 
} 
+0

爲什麼不把所有的琴絃都放在一起? – Awn

+0

@Awn我在我編寫的包中做了一些字符串,但是我試圖解決的問題是如何在從其他地方通過一個字符串時清零字符串的內存。不幸的是,在純Go中沒有任何有保證的方式來做到這一點,但這不會阻止我至少嘗試:) –

2

在內存中處理安全值在Go難度比這將是類似於C或C++。那是因爲GC,它隨時隨地複製和混淆了它的任何內存。

因此,第一步是獲得一些GC不能混淆的內存。爲此,我們要麼旋轉cgo和malloc,無論我們想要什麼;或者使用像mmap和VirtualAlloc這樣的系統調用;然後像平常一樣繞過結果切片。

下一步是告訴操作系統你不希望這個內存被換出到磁盤,所以你mlock或VirtualLock它。

在退出之前,用libsodium或者直接遍歷它,將每個元素設置爲零來置零。這對字符串是不可能的,我不確定我會建議手動擦除字符串的內存。我的意思是,我不能立即發現它有什麼問題,但是......它感覺不對。無論如何,沒有人使用字符串作爲安全值。

有一個庫(我的)是專門爲存儲安全值而設計的,它和上面描述的一起做了一些其他事情。你可能會覺得它很有用:https://github.com/awnumar/memguard

+1

感謝您使用cgo,Awn的鏈接和建議。這個memguard包看起來很有趣。自從我在考慮問題的背景下思考記憶以來已經有一段時間了,但我想我會在下一次獲得的機會中開始使用該包。 –

相關問題