Golang實現Python Django2密碼算法pbkdf2_sha256
0. 說明
系統環境
go version go1.14.3 darwin/amd64
因爲項目需要從Python Django
框架重構爲Golang
項目,爲了保證用戶數據不丟失,所以密碼算法使用與Django框架相同的pbkdf2_sha256
加密算法。
以下代碼根據GitHub
開源項目github.com/alexandrevicenzi/unchained/
修改而成,有興趣的可以翻閱項目代碼。
1. 簡單的加密算法分析
直接拿出一個經過pbkdf2_sha256
加密後的密鑰
pbkdf2_sha256$180000$9v62SVRUKPTf$igFByaMbYXhOh7p375LDdo2GYrUV9RbqdTaR6jbhrKg=
密鑰一共分爲4個部分,分別是加密算法名稱
、加密迭代次數
、鹽(salt)
、base64編碼
,每一個部分都由美元$
符號分割,由此我們解析上面密鑰的數據。
- 加密算法名稱:pbkdf2_sha256
- 加密迭代次數:180000
- 鹽:9v62SVRUKPTf
- Base64編碼:igFByaMbYXhOh7p375LDdo2GYrUV9RbqdTaR6jbhrKg=
由於pbkdf2_sha256
屬於單向
加密算法(即無法通過密鑰反推原始密碼),所以我們要校驗密碼需要用同樣的方式來加密,最終將兩個加密密鑰對比,如果相同則認爲密碼正確,整體流程如下所示。
2. 加密
需要使用加密算法庫golang.org/x/crypto/pbkdf2
,控制檯輸入如下命令:
go get golang.org/x/crypto/pbkdf2
加密過程我們需要三個參數,分別是原始密碼
、鹽(Salt)
、加密迭代次數
,其中鹽
我們可以隨機生成(注意:鹽
不能包含美元$
符號,不然會被誤認爲是分隔符),加密迭代次數
可以隨意設置,一般情況下越大則越難被破譯,但開銷也越大,Django選用180000
次,這裏不深入討論。
func PasswordEncode(password string, salt string, iterations int) (string, error) {
// 一共三個參數,分別是原始密碼、鹽、迭代次數
// 如果沒有設置鹽,則使用12位的隨機字符串
if strings.TrimSpace(salt) == "" {
salt = CreateRandomString(12)
}
// 確保鹽不包含美元$符號
if strings.Contains(salt, "$") {
return "", errors.New("salt contains dollar sign ($)")
}
// 如果迭代次數小於等於0,則設置爲180000
if iterations <= 0 {
iterations = 180000
}
// pbkdf2加密 <--- 關鍵
hash := pbkdf2.Key([]byte(password), []byte(salt), iterations, sha256.Size, sha256.New)
// base64編碼成爲固定長度的字符串
b64Hash := base64.StdEncoding.EncodeToString(hash)
// 最終字符串拼接成pbkdf2_sha256密鑰格式
return fmt.Sprintf("%s$%d$%s$%s", "pbkdf2_sha256", iterations, salt, b64Hash), nil
}
// 隨機字符串生成函數(不深入討論)
func CreateRandomString(len int) string {
var container string
var str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
b := bytes.NewBufferString(str)
length := b.Len()
bigInt := big.NewInt(int64(length))
for i := 0; i < len; i++ {
randomInt, _ := rand.Int(rand.Reader, bigInt)
container += string(str[randomInt.Int64()])
}
return container
}
3. 校驗
我們只需要通過將原始密碼重新使用按照相同的鹽
和迭代次數
加密就能獲得對應的密鑰,將我們得到的密鑰和數據庫中的密鑰對比,如果相同則認爲是正確的密碼,如果不相同則認爲是不正確的密碼。
func PasswordVerify(password string, encoded string) (bool, error) {
// 輸入兩個參數,分別是原始密碼、需要校驗的密鑰(數據庫中存儲的密碼)
// 輸出校驗結果(布爾值)、錯誤
// 先根據美元$符號分割密鑰爲4個子字符串
s := strings.Split(encoded, "$")
// 如果分割結果不是4個子字符串,則認爲不是pbkdf2_sha256算法的結果密鑰,跳出錯誤
if len(s) != 4 {
return false, errors.New("hashed password components mismatch")
}
// 分割子字符串的結果分別爲算法名、迭代次數、鹽和base64編碼
// ---> 這裏可以獲得加密用的鹽
algorithm, iterations, salt := s[0], s[1], s[2]
// 如果密鑰算法名不是pbkdf2_sha256算法,跳出錯誤
if algorithm != "pbkdf2_sha256" {
return false, errors.New("algorithm mismatch")
}
// 將迭代次數轉換成int數據類型 -->這裏可以獲得加密用的迭代次數
i, err := strconv.Atoi(iterations)
if err != nil {
return false, errors.New("unreadable component in hashed password")
}
// 將原始密碼用上面獲取的鹽、迭代次數進行加密
newEncoded, err := PasswordEncode(password, salt, i)
if err != nil {
return false, err
}
// 最終用hmac.Equal函數判斷兩個密鑰是否相同
return hmac.Equal([]byte(newEncoded), []byte(encoded)), nil
}