You are already familiar with hashing, and know that through hash functions we can compute the hash value of a string and even the hash value of a file. The next step is to find out how we can use Go to compute hash values.
In this topic, we'll learn how to use the crypto package to compute hash values with some of the most common hashing algorithms.
Hashing
As you might remember, a hashing algorithm is a cryptographic hash function. It is a mathematical algorithm that maps data of arbitrary size to fixed-size values. Let's take a brief look at the most common hashing algorithms:
MD5: Created in 1992 by Ronald Rivest, it takes any input and outputs a 128-bit hash value. Even though MD5 is no longer considered secure for high-level security use cases, such as password storage, it is still broadly used nowadays to verify files' integrity.
SHA-256: Part of the SHA-2 family of functions, created by the NSA in 2001. It produces a 256-bit hash value and is mostly used in authentication and encryption protocols, such as SSL and SSH. It is also used for other applications, such as the management of Bitcoin addresses and transactions.
SHA-512: Another member of the SHA-2 family of functions; it produces a longer 512-bit hash value, and is commonly used for email address/password hashing and digital record verification. SHA-512 is not as broadly used as SHA-256, because it produces bigger outputs, thus requiring more bandwidth to store and transmit data.
bcrypt: A password-hashing function, designed by Niels Provos and David Mazières based on the Blowfish cipher and first published in 1999. It is considered a secure hashing function because it requires a salt — a random string added to the plaintext password; this added "salt" makes the computed hash value unpredictable.
Computing the hash value of a string
Now that you've been acquainted with some of the most common hashing algorithms, let's take a look at the most basic hashing operation — computing the hash value of a string in Go:
package main
import (
"crypto/md5"
"crypto/sha256"
"crypto/sha512"
"fmt"
)
func main() {
md5Hash := md5.New()
sha256Hash := sha256.New()
sha512Hash := sha512.New()
md5Hash.Write([]byte("JetBrains Academy"))
sha256Hash.Write([]byte("JetBrains Academy"))
sha512Hash.Write([]byte("JetBrains Academy"))
fmt.Printf("%x\n", md5Hash.Sum(nil))
fmt.Printf("%x\n", sha256Hash.Sum(nil))
fmt.Printf("%x\n", sha512Hash.Sum(nil))
}
// Output:
// dc5740934090c9ed7aa0b3ec8ac755f3
// 83ac28f753df3cd80fee3f8ce1770da805856afa2b48c2917aefe5123723c4c9
// 97e5ee749844c330b4e99779bf2d6487cd22497fcff0c49cc2d736fcf95374d1...
Let's examine the above code; the first thing you might notice is that the crypto package itself does not contain any hashing algorithms, so we need to import the sub-packages md5, sha256, and sha512.
The next step is to create a new interface of the Hash type with the New() function. The most common methods of the Hash interface are the following:
-
Write()— It takes a slice of bytes as an argument and adds it to the hash that will be calculated. -
Sum()— Appends a slice of bytes to the current hash and returns the resulting slice. Take notice that in the above example we don't want to append any additional data to the hash we initially passed toWrite(), so we just passnilas an argument toSum(). -
Reset()— Resets the hash to one with zero bytes written.
Finally, to output the computed hash properly, we need to use the %x verb. It allows us to format the resulting slice of the Sum() method in hexadecimal notation.
Notice that the output hash value of the SHA-512 has been shortened because it's quite lengthy. If you want to see the full output, you can run the above example in the Go Playground.
Using bcrypt for password hashing
You might be wondering why we didn't showcase the bcrypt algorithm in the previous section. The explanation is simple: since bcrypt is not part of the crypto package of Go's standard library, we need to use the go get command to add it to our Go project:
$ go get golang.org/x/crypto/bcrypt
After installing the bcrypt package, we'll be able to import and use it within our Go program:
package main
import (
"fmt"
"log"
"golang.org/x/crypto/bcrypt"
)
func main() {
password := "TrustNo1"
b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b)) // $2a$14$VCVRDFps04C6lmbRTgmDV.G2L9kZdwxfBfFJzXCEQdNSoTsEdn4Nm
}
Computing the hash value of a string (in this case, a password) with the bcrypt package is pretty straightforward. We just need to call the GenerateFromPassword() function and pass two arguments to it — a slice of bytes password and a specific cost.
The cost argument is the hashing cost used to create the given password. The higher the value of cost, the more computational power will be required to compute the password hash. In this case, we pass the constant bcrypt.DefaultCost with the cost of 10.
Keep in mind that the bcrypt package has a series of constants defined for costs: bcrypt.MinCost = 4, bcrypt.MaxCost = 31, and bcrypt.DefaultCost = 10. If we pass a cost lower than MinCost to GenerateFromPassword(), the program will pass DefaultCost instead.
GenerateFromPassword() returns a slice of bytes b and an error err, thus, to properly output the computed hash as a string, we need to cast b as a string within the fmt.Println() function.
Comparing a bcrypt hash and a plaintext password
So far, we've seen how to compute the hash value of a password. However, what if we wanted to compare the computed hash with its possible plaintext equivalent?
The bcrypt package provides a very useful function for this purpose — CompareHashAndPassword(). Let's take a look at how we can use it within our Go program:
...
func main() {
... // Compute the hash value of a certain password
var enteredPassword string
fmt.Scanln(&enteredPassword) // Ask the user to enter a plaintext password
// Compares the 'bcrypt' hashed password with its possible plaintext equivalent:
if err := bcrypt.CompareHashAndPassword(b, []byte(enteredPassword)); err != nil {
log.Fatal(err) // Exit the program if the hashes of the two passwords do not match
}
fmt.Println("Passwords match!")
}
// Input: TrustNo1
// Output: Passwords match!
// Input: TrustNoOne
// Output: 2022/03/29 22:11:45 crypto/bcrypt: hashedPassword is not the hash of the given password
The above example showcases a simple way to use the CompareHashAndPassword() function. After we've computed the hash value of a certain password, we can pass a plaintext string containing a password to CompareHashAndPassword(), and it will compare the previously bcrypt hashed password with the possible equivalent. The function returns nil on success and an error err on failure.
Computing the hash value of a file
Apart from computing the hash value of a string, we can also compute the hash value of a file, with the most common use case being verifying a certain file's integrity. Let's take a look at how we can use the previously mentioned hashing algorithms to compute the hash value of the hello.txt file in Go:
...
func main() {
file, err := os.Open("hello.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
md5Hash := md5.New()
// Copy the data from 'hello.txt' to the 'md5Hash' interface until reaching EOF:
if _, err := io.Copy(md5Hash, file); err != nil {
log.Fatal(err)
}
fmt.Printf("%x\n", md5Hash.Sum(nil)) // e0ede7b570137d20b86ad570db938236
}
First, we need to open hello.txt in read-only mode via the os.Open() function. Then, we create a new Hash interface with the New() function within the md5Hash variable.
The next step is the most important part: we need to call the io.Copy() function from the io package. It will take as an argument a destination, which will be the md5Hash interface, and a source file to read data from. The io.Copy() function will then copy data from the source to the destination until we've reached the end of the hello.txt file.
After copying all the data to the md5Hash interface and reaching the end of the file, we can finally print the computed hash returned by the Sum() function, using the %x verb.
An important detail is that we can't use the bcrypt hashing function to calculate the hash value of a file, as it is specifically used to encrypt password strings. However, we can use md5, sha256, and sha512 to calculate the hash value of files.
Conclusion
In this topic, we've learned about the most common hashing algorithms and how to use the crypto package along with its sub-packages to compute hash values for both strings and files in Go.
In particular, we've covered how to perform the following hashing operations:
-
Creating a
Hashinterface withNew()function of thecrypto/md5,crypto/sha256, andcrypto/512packages; -
Using the
Write()andSum()methods of theHashinterface to compute the hash value of a string; -
How to install the
bcryptexternal package using thego getcommand, and then using thebcrypt.GenerateFromPassword()function to compute the hash value of a password; -
How to compare a
bcrypthash with a plaintext password, using thebcrypt.CompareHashAndPassword()function; -
Computing the hash value of a file by copying the data from the file to a previously created
Hashinterface, using theio.Copy()function.
We've also learned that the bcrypt hashing algorithm is used specifically to compute hash values for password strings and we can't use it to calculate the hash value of a file.
Great job, but we're not done yet! Let's go ahead and test our newly acquired knowledge on hashing strings and files with some theory and coding tasks!