我目前在/etc/shadow
文件中使用的是$6$IcnB6XpT8xjWC$AI9Rq5hqpEP.Juts/TUbHk/OI7sO/S1AA.ihgBjHN12QmT5p44X5or86PsO9/oPBO4cmo0At4XuMC0yCApo87/
格式的密码(sha512
加密密码)。我需要做的是验证密码(用户提供的密码和当前散列密码)。我正在Go
上实现这个。
我是如何做到这一点的,获得用户提供的密码,哈希它与什么是在/etc/shadow
相同的盐,并检查它们是否相似或不同。如何生成相同的哈希值并验证密码?
下面是我正在做的粗略代码(更新了Amadan对编码的评论)
// this is what is stored on /etc/shadow - a hashed string of "test"
myPwd := "$6$IcnB6XpT8xjWC$AI9Rq5hqpEP.Juts/TUbHk/OI7sO/S1AA.ihgBjHN12QmT5p44X5or86PsO9/oPBO4cmo0At4XuMC0yCApo87/"
// getting the salt
salt := strings.Split(myPwd, "$")[2]
encoding := base64.NewEncoding("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz").WithPadding(base64.NoPadding)
decodedSalt, err := encoding.DecodeString(salt)
// user provided password
myNewPwd := "test"
newHashedPwd, err := hashPwd512(string(myNewPwd), string(decodedSalt))
// comparision
if (newHashedPwd == myPwd) {
// password is same, validate it
}
// What I'm expecting from this method is that for the same password stored in the /etc/shadow,
// and the same salt, it should return (like the one in /etc/shadow file)
// AI9Rq5hqpEP.Juts/TUbHk/OI7sO/S1AA.ihgBjHN12QmT5p44X5or86PsO9/oPBO4cmo0At4XuMC0yCApo87/
func hashPwd512(pwd string, salt string) (string, error) {
hash := sha512.New()
hash.Write([]byte(salt))
hash.Write([]byte(pwd))
encoding := base64.NewEncoding("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz").WithPadding(base64.NoPadding)
hashedPwd := encoding.EncodeToString(hash.Sum(nil))
return hashedPwd, nil
}
备注:通过passwd
或chpasswd
设置/修改密码。
SHA512是一个快速哈希。这对于密码来说很糟糕,因为当每次尝试几乎不花费任何代价时,暴力破解变得非常容易。为了降低速度,/etc/shadow
中的内容不仅仅是简单的SHA512哈希,而是在哈希算法运行数千次的情况下应用密钥拉伸。具体的键拉伸算法似乎是这个。因此,crypto/sha512
只完成了所需的1/5000(在默认情况下)。
幸运的是,有人已经做了艰苦的工作;你可以在这里看到它们的实现。
package main
import (
"fmt"
"strings"
"github.com/GehirnInc/crypt"
_ "github.com/GehirnInc/crypt/sha512_crypt"
)
func main() {
saltedPass := "$6$IcnB6XpT8xjWC$AI9Rq5hqpEP.Juts/TUbHk/OI7sO/S1AA.ihgBjHN12QmT5p44X5or86PsO9/oPBO4cmo0At4XuMC0yCApo87/"
fmt.Println("Original: ", saltedPass)
// Make new hash from scratch
plainPass := "test"
crypt := crypt.SHA512.New()
newSaltedPass, err := crypt.Generate([]byte(plainPass), []byte(saltedPass))
if err != nil {
panic(err)
}
fmt.Println("Generated:", newSaltedPass)
// Verify a password (correct)
err = crypt.Verify(saltedPass, []byte(plainPass))
fmt.Println("Verification error (correct password): ", err)
// Verify a password (incorrect)
badPass := "fail"
err = crypt.Verify(saltedPass, []byte(badPass))
fmt.Println("Verification error (incorrect password):", err)
}
由于您只需要验证,Verify
快捷方式就足够了(它在幕后为您完成Generate
):
err = crypt.Verify(saltedPass, []byte(plainPass))
if err == nil {
fmt.Println("Fly, you fools!")
} else {
fmt.Println("You shall not pass!")
}
输出:
Original: $6$IcnB6XpT8xjWC$AI9Rq5hqpEP.Juts/TUbHk/OI7sO/S1AA.ihgBjHN12QmT5p44X5or86PsO9/oPBO4cmo0At4XuMC0yCApo87/
Generated: $6$IcnB6XpT8xjWC$AI9Rq5hqpEP.Juts/TUbHk/OI7sO/S1AA.ihgBjHN12QmT5p44X5or86PsO9/oPBO4cmo0At4XuMC0yCApo87/
Verification error (correct password): <nil>
Verification error (incorrect password): hashed value is not the hash of the given password
注意:我认为我在评论中是错误的。密码哈希本身是由这种编码编码的,但盐似乎是实际的盐(只是当它随机生成时,使用该编码中的字符)。
库还支持其他散列函数,但是您需要为每个函数导入一个来注册它。你也可以看到,我没有费心从saltedPass
中分离盐;这也是你不需要担心的。
但是,如果您出于某种原因想要隔离盐,那么还要注意,从一开始就计算$
并不是一个安全的主意,因为它将无法正确处理像$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0g
这样的条目,例如。请使用strings.LastIndex(saltedPass, "$") + 1
作为切点。