Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

Wrapping packages to isolate code responsibility


When writing code in Go, or really any language, you will often find yourself using other packages to get things done. For example, you might use the net/http package as a basis for building a web server rather than writing all that code yourself.

From time to time you might notice that the third party package doesn't quite behave in a manner that is aligned with how you intend to use the package. One example of this is the golang.org/x/crypto/bcrypt package; When I use it, I am often dealing with strings in my own application, but every method in the crypto/bcrypt package expects a byte slice as input. While this isn't necessarily a problem, it can lead to code duplication that is unnecessary.

Another frequent occurrence is wanting to add some functionality on top of what is being offered by another package. Looking at the crypto/bcrypt package once again, this is a package that handles hashing and salting passwords, but it would be especially great if the package took care of peppering our passwords as well.

In this article we are going to explore a technique that can be used to simplifying your code that interacts with third party libraries by wrapping those packages into your own package. We will be doing this using the crypto/bcrypt package, but the same pattern can be applied to other packages and types.

Learn more about how to get the most out of Go while developing web applications.Sign up for my mailing listand I'll send you the first three chapters from my upcoming book, Web Development with Go. I'll also occasionally send you emails when I publish new blog posts like this one.

Using the bcrypt package

Before we can jump into wrapping the crypto/bcrypt package we first need to take a look at how you might use it inside of an existing application. To do this, we are going to look at two different functions provided by the crypto/bcrypt package:

GenerateFromPassword - given a raw password in the form of a byte slice and a cost, this functions generates a hashed and salted password using the bcrypt algorithm . CompareHashAndPassword - given a hashed password and a raw password, both as byte slices, this compares the two passwords in a time-constant manner and returns an error if they don't match for any reason.

We are also going to assume that our application is making use of a pepper variable, which is similar to a salt but is application-wide and is intended to help prevent attackers from getting passwords when they only get access to a database and not the actual source code. We will also assume that we are dealing with strings, so both the incoming raw password and the final hashed password will need to be strings.

Putting that all together, you might end up with some code like below to generate your hashed password.

rawPassword := "some-pw" pepper := "some-pepper" hashedBytes, err := bcrypt.GenerateFromPassword( []byte(rawPassword + pepper), bcrypt.DefaultCost) if err != nil { // This really shouldn't happen unless we provide an invalid bcrypt cost or // are doing something else very wrong } // Use the hashedBytes, or convert to a string hashedString := string(hashedBytes)

And then you might write something like the code below to compare your password to a hashed one. For simplicity's sake, we are just assigning the hashed password to a string, but in most practical cases you would get this from a datastore of some sort.

rawPw := "some-pw" pepper := "some-pepper" hashedPw := "$2a$10$aSjYOncUeas4wV3e5vjb8eBT4lKrev3vMjvTATAYlGqY3m6RHnHFa" err := bcrypt.CompareHashAndPassword( []byte(hashedPw), []byte(rawPw+pepper)) if err != nil { // Passwords don't match - tell the user the login was invalid! } // The passwords match if no error!

NOTE: This code sample isn't complete, but is meant to give you an idea of how you would use the CompareHashAndPassword() function

Now that we know what we are working with we can start to look for ways to wrap the crypto/bcrypt package and simplify our code.

Figure out what common tasks you do a lot

The first thing I suggest doing is looking for common tasks that you are performing in order to utilize the package. In this case, it looks like we are converting between byte slices and strings a good bit. Another thing we are doing is messing with a pepper a lot - when we create and compare passwords we are appending a pepper to the strings every time. One final thing that we might consider is changing the return value of the the CompareHashAndPassword() function; I often end up treating this value as a true/false statement (checking if it is nil), so we will be updating that function to return a bool rather than an error.

Now that we know have an idea of what code we are writing a lot we can start to look at how we might simplify this.

It is important to note that we didn't try to simplify this first! We first wrote the code that uses the bcrypt package, and then once we had a realistic idea of what we could do simplify we took some notes and then proceeded to refactor our code.

Support those common tasks in your own package

The first thing we are going to do is declare a new type inside of our own hash package. In many cases you won't need to create a new type and can instead just offer functions to get the job done, but in this case I feel that a struct is more appropriate because we are going to store a pepper in the struct. Doing this will make it much easier to just pass around a copy of our BCrypt object to code that needs to handle passwords, and they won't have to deal with a pepper directly.

package hash // We will use this shortly, so I am showing it for now. import "golang.org/x/crypto/bcrypt" func NewBCrypt(pepper string) BCrypt { return BCrypt{[]byte(pepper)} } type BCrypt struct { pepper []byte }

I also created a NewBCrypt() function to be used when constructing a new BCrypt object, which handles converting the incoming pepper string into a byte slice. Again, this is useful because in my application I typically tend to be working with strings in stead of byte slices for my pepper.

Next we will implement a function used to compare a raw password with a hashed password. In the crypto/bcrypt package this function is titled CompareHashAndPassword() , but we are free to name it whatever we want, and I tend to prefer using something a little shorter like Equal() . As I mentioned before, this will return a boolean instead of an error.

func (bc BCrypt) Equal(hash, rawPw string) bool { err := bcrypt.CompareHashAndPassword( []byte(hash), append([]byte(rawPw), bc.pepper...)) return err == nil }

Obviously this isn't the most performant code in the world because it has to convert strings to bytes, but if we are already working with strings in our application chances are we would need to do this anyway, and this does make dealing with our strings much easier.

Moving on, we are going to create a method used to hash our passwords - aptly named Hash() . Once again, this will deal with strings instead of byte slices, but in this case we will still return any errors that occur because our upstrem code will likely need to know about them.

func (bc BCrypt) Hash(input string) (string, error) { bytes, err := bcrypt.GenerateFromPassword( append([]byte(input), bc.pepper...), bcrypt.DefaultCost) if err != nil { return "", err } return string(bytes), nil }

Now that we have our new BCrypt type written, let's go back to our original code and see how we might use this to simplify our original code.

What does it look like to use our new type?

The first thing we need to do is construct a new instance of our BCrypt type. That will give us the ability to simply pass this around instead of passing around our pepper.

bc := NewBCrypt("some-pepper")

After that we can utilize the bc object to hash our password.

rawPw := "some-pw" hashedPw, err := bc.Hash(rawPw) if err != nil { panic(err) }

And finally we can use the bc object to compare our hashed password with a raw password to determine if the user has provided us with valid credentials.

if bc.Equal(hashedPw, rawPw) { fmt.Println("They match!") }

Putting it all together we get something like the code below.

package main import ( "fmt" "calhoun.io/hash" ) func main() { bc := hash.NewBCrypt("some-pepper") rawPw := "some-pw" hashedPw, err := bc.Hash(rawPw) if err != nil { panic(err) } if bc.Equal(hashedPw, rawPw) { fmt.Println("They match!") } }

Now that is much cleaner. Our code no longer has to think about a bcrypt cost, adding a pepper to passwords, or even converting strings to byte slices. This is really important for maintaining code in because it means that any future changes that might alter the cost you are using for bcrypt won't need to be spread across multiple files, but instead could be a single change to the BCrypt type. You could also make the cost an argument when calling NewBCrypt .

You can do this for many different packages

As I said before, you can do this for a large variety of packages. For example, you might find that you need to use HMAC in your code, but you need to store a base64 encoded version of the HMACs rather than a byte slice. That is easily achieved with the code below.

package hash import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "hash" ) func NewHMAC(key string) HMAC { h := hmac.New(sha256.New, []byte(key)) return HMAC{h} } type HMAC struct { hash.Hash } func (h HMAC) Equal(hash, raw string) bool { hashMac, err := base64.URLEncoding.DecodeString(hash) if err != nil { // Invalid MAC provided return false } rawMac := h.Bytes([]byte(raw)) return hmac.Equal(hashMac, rawMac) } func (h HMAC) Bytes(input []byte) []byte { h.Reset() h.Write(input) return h.Sum(nil) } func (h HMAC) String(input string) string { // Leverage the existing Bytes() method return base64.URLEncoding.EncodeToString(h.Bytes([]byte(input))) }

Or you might have a custom type and decide that you want to make sorting slices of that type easier. No problemo! We don't even have to export a new type to achieve this.

package sort import ( "sort" "calhoun.io/things" ) // calhoun.io/things defines the Dog type: // // type Dog struct { // Age int // } func Dogs(a []things.Dog) { sort.Sort(dogs(a)) } type dogs []things.Dog func (d dogs) Len() int { return len(d) } func (d dogs) Less(i, j int) bool { return d[i].Age < d[j].Age } func (d dogs) Swap(i, j int) { d[i], d[j] = d[j], d[i] } In summary...

The next time you finish writing some code, I encourage you to take a moment to consider how you could clean up your code and make it more maintainable.

While this pattern not always be a great fit, it is a great way to help isolate responsibilities inside your application, which typically leads to a more maintainable codebase.

Want to learn about how to apply tricks like this to make a clean and maintainable web application?

Sign up for my mailing listand I'll send you THREE FREE CHAPTERS from my book, Web Development with Go. In it you will not only learn about how to build web applications with Go, but you will also see how to apply different techniques to make your code a breeze to maintain.

Viewing all articles
Browse latest Browse all 12749