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
.
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.