英文:
Validate password against the hashed password in /etc/shadow file using Go
问题
我目前在/etc/shadow
文件中有一个密码,格式如下:$6$IcnB6XpT8xjWC$AI9Rq5hqpEP.Juts/TUbHk/OI7sO/S1AA.ihgBjHN12QmT5p44X5or86PsO9/oPBO4cmo0At4XuMC0yCApo87/
(一个sha512
加密的密码)。我需要做的是验证密码(用户提供的密码和当前的哈希密码)。我正在使用Go
实现这个功能。
我尝试的方法是,获取用户提供的密码,使用与/etc/shadow
中相同的盐值进行哈希,并检查它们是否相似或不同。我如何生成相同的哈希值并验证密码?
以下是我正在进行的大致代码(根据Amadan的评论进行了更新):
// 这是存储在/etc/shadow中的哈希字符串,表示"test"
myPwd := "$6$IcnB6XpT8xjWC$AI9Rq5hqpEP.Juts/TUbHk/OI7sO/S1AA.ihgBjHN12QmT5p44X5or86PsO9/oPBO4cmo0At4XuMC0yCApo87/"
// 获取盐值
salt := strings.Split(myPwd, "$")[2]
encoding := base64.NewEncoding("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz").WithPadding(base64.NoPadding)
decodedSalt, err := encoding.DecodeString(salt)
// 用户提供的密码
myNewPwd := "test"
newHashedPwd, err := hashPwd512(string(myNewPwd), string(decodedSalt))
// 比较
if newHashedPwd == myPwd {
// 密码相同,进行验证
}
// 我期望这个方法返回的是与/etc/shadow文件中相同的密码(例如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
设置/更改的。
英文:
What I have currently in /etc/shadow
file is password in a format like $6$IcnB6XpT8xjWC$AI9Rq5hqpEP.Juts/TUbHk/OI7sO/S1AA.ihgBjHN12QmT5p44X5or86PsO9/oPBO4cmo0At4XuMC0yCApo87/
(a sha512
encrypted password). What I need to do is validate the password (user provided password and the current hashed password). I'm implementing this on Go
.
How I'm trying to achieve this is, get the user provided password, hash it with the same salt as what is in the /etc/shadow
and check if they are similar or different. How can I have generate the same hash value and validate the password?
Below is the rough code what I'm doing (updated with Amadan's comment on encoding)
// 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
}
Note: The password is set/changed through passwd
or chpasswd
.
答案1
得分: 2
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)
// 从头开始生成新的哈希
plainPass := "test"
crypt := crypt.SHA512.New()
newSaltedPass, err := crypt.Generate([]byte(plainPass), []byte(saltedPass))
if err != nil {
panic(err)
}
fmt.Println("Generated:", newSaltedPass)
// 验证密码(正确)
err = crypt.Verify(saltedPass, []byte(plainPass))
fmt.Println("Verification error (correct password): ", err)
// 验证密码(错误)
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
作为切割点。
英文:
SHA512 is a fast hash. This is bad for passwords, because brute force becomes very easy when each attempt costs almost nothing. To slow it down, what is in /etc/shadow
is not just a simple SHA512 hash, but rather key stretching is applied where the hash algorithm is run thousands of times. The specific key stretching algorithm seems to be this one. Thus, crypto/sha512
is only doing about 1/5000th of what is needed (in the default case).
Fortunately, there's people who already did the hard work; you can see their implementation here.
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)
}
Since you only need validation, the Verify
shortcut suffices (it does Generate
behind the scenes for you):
err = crypt.Verify(saltedPass, []byte(plainPass))
if err == nil {
fmt.Println("Fly, you fools!")
} else {
fmt.Println("You shall not pass!")
}
Output:
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
NOTE: I believe I was incorrect in the comments. The password hash itself is encoded by such an encoding, but the salt seems to be the actual salt (it's just that when it is generated randomly, the characters from that encoding are used).
The library also supports other hashing functions, but you do need an import for each one to register it. You can also see that I did not bother isolating the salt from saltedPass
; that's also something you don't need to worry about.
But if you do want to isolate the salt, for some reason, then note also that counting the $
from the start is not a safe idea, as it will fail to process entries like $6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0g
correctly, for example. Use strings.LastIndex(saltedPass, "$") + 1
as a cutting point, instead.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论