Yusuff Faruq Frontend web developer and anime lover from Nigeria.

Cryptography in Go today

6 min read 1742

Go Logo

Cryptography is the practice and study of techniques for secure communication in the presence of third-party adversaries. In web applications, developers use cryptography to ensure that user data is secure and that the system is not exploited by bad actors who might be looking to take advantage of loopholes in the system for personal gain.

Most programming languages have their own implementation of common cryptography primitives, algorithms, and so on. In this article, we will be taking a look at how cryptography is handled in the Go programming language and what cryptography packages are available today.

To begin, let’s take a look at the crypto package in the standard Go library.

Go’s standard crypto package

If you’ve been writing Go for a decent amount of time, you would agree that the standard Go library is amazingly robust, covering things from HTTP to encoding and even testing. So it should come as no surprise that Go comes with its own cryptography package.

The crypto package itself contains common cryptographic constants, implementations for basic cryptographic principles, and so on. Most of its worth, however, lies in its subpackages. The crypto package has a variety of subpackages, each of which focuses on a single cryptographic algorithm, principle, or standard.

We have the aes package, which focuses on AES (Advanced Encryption Standard); hmac, which focuses on HMAC (hash-based message authentication code) for digital signatures and verification; and many others. With these packages, we can perform different cryptography-related tasks like encryption, decryption, hashing, etc. Let’s explore at how we’d do that.

Hashing

Hashing is basically the process of taking an input of an arbitrary size and producing an output of a fixed size. At the very least, a good hashing algorithm will never produce the same output for two different inputs and will always produce the same output for a given input.

There are a number of different hashing algorithms, such as SHA-256, SHA-1, and MD5 — all of which are supported in the Go crypto package — as well as several others. Here’s an implementation of a function that hashes plaintext using the SHA-256 hashing algorithm and returns the hash in hexadecimal format.

func hashWithSha256(plaintext string) (string, error) {
   h := sha256.New()
   if _, err := io.WriteString(h, plaintext);err != nil{
      return "", err
   }
   r := h.Sum(nil)
   return hex.EncodeToString(r), nil
}

func main(){
  hash, err := hashWithSha256("hashsha256")
  if err != nil{
     log.Fatal(err)
  }
  fmt.Println(hash)  //c4107b10d93310fb71d89fb20eec1f4eb8f04df12e3f599879b03be243093b14
}

As you can see, the New function of the sha256 subpackage returns a type that implements the Hash interface. Any type that implements this interface also implements the Writer interface. Therefore, we can simply write our plaintext to it, get the checksum using the Sum method, and encode the result in hexadecimal format.

This code works with other hashing algorithms, too — you just need to create a Hash from the appropriate package. So if we were hashing using the MD5 algorithm, we would have:

We made a custom demo for .
No really. Click here to check it out.

h := md5.New()

Symmetric-key cryptography

We can also implement symmetric-key cryptography using only the Go standard library. Symmetric-key cryptography simply involves encrypting plaintext and decrypting the corresponding ciphertext with the same key.

With the Go crypto package, we can make use of stream and block ciphers for encryption and decryption. Let’s take a look at how we can implement symmetric-key cryptography using AES with the CBC (cipher block chaining) mode.

First of all, we write a function to create a new block cipher with a given key. AES takes only keys with key lengths of 128, 192, or 256 bits. So we will hash the given key and pass the hash as the key of our block cipher. This function returns a Block from the cipher subpackage and an error.

func newCipherBlock(key string) (cipher.Block, error){
   hashedKey, err := hashWithSha256(key)
   if err != nil{
      return nil, err
   }
   bs, err := hex.DecodeString(hashedKey)
   if err != nil{
      return nil, err
   }
   return aes.NewCipher(bs[:])
}

Before we start writing the functions for encryption and decryption, we need to write two functions for padding and unpadding our plaintext. Padding is simply the act of increasing the length of plaintext so that it can be a multiple of a fixed size (usually a block size). This is usually done by adding characters to the plaintext.

There are different padding schemes, and since Go does not automatically pad plaintext, we have to do that ourselves. This GitHub gist by user huyinghuan shows an easy way to pad plaintext using the PKCS7 padding scheme, which was defined in section 10.3 of RFC 2315.

var (
   // ErrInvalidBlockSize indicates hash blocksize <= 0.
   ErrInvalidBlockSize = errors.New("invalid blocksize")

   // ErrInvalidPKCS7Data indicates bad input to PKCS7 pad or unpad.
   ErrInvalidPKCS7Data = errors.New("invalid PKCS7 data (empty or not padded)")

   // ErrInvalidPKCS7Padding indicates PKCS7 unpad fails to bad input.
   ErrInvalidPKCS7Padding = errors.New("invalid padding on input")
)

func pkcs7Pad(b []byte, blocksize int) ([]byte, error) {
   if blocksize <= 0 {
      return nil, ErrInvalidBlockSize
   }
   if b == nil || len(b) == 0 {
      return nil, ErrInvalidPKCS7Data
   }
   n := blocksize - (len(b) % blocksize)
   pb := make([]byte, len(b)+n)
   copy(pb, b)
   copy(pb[len(b):], bytes.Repeat([]byte{byte(n)}, n))
   return pb, nil
}

func pkcs7Unpad(b []byte, blocksize int) ([]byte, error) {
   if blocksize <= 0 {
      return nil, ErrInvalidBlockSize
   }
   if b == nil || len(b) == 0 {
      return nil, ErrInvalidPKCS7Data
   }

   if len(b)%blocksize != 0 {
      return nil, ErrInvalidPKCS7Padding
   }
   c := b[len(b)-1]
   n := int(c)
   if n == 0 || n > len(b) {
      fmt.Println("here", n)
      return nil, ErrInvalidPKCS7Padding
   }
   for i := 0; i < n; i++ {
      if b[len(b)-n+i] != c {
         fmt.Println("hereeee")
         return nil, ErrInvalidPKCS7Padding
      }
   }
   return b[:len(b)-n], nil
}

Now that we have got that down, we can write the functions for encryption and decryption.

//encrypt encrypts a plaintext
func encrypt(key, plaintext string) (string, error) {
   block, err := newCipherBlock(key)
   if err != nil {
      return "", err
   }

  //pad plaintext
   ptbs, _ := pkcs7Pad([]byte(plaintext), block.BlockSize())

   if len(ptbs)%aes.BlockSize != 0 {
      return "",errors.New("plaintext is not a multiple of the block size")
   }

   ciphertext := make([]byte, len(ptbs))

  //create an Initialisation vector which is the length of the block size for AES
   var iv []byte = make([]byte, aes.BlockSize)
   if _, err := io.ReadFull(rand.Reader, iv); err != nil {
      return "", err
   }

   mode := cipher.NewCBCEncrypter(block, iv)

  //encrypt plaintext
   mode.CryptBlocks(ciphertext, ptbs)

  //concatenate initialisation vector and ciphertext
   return hex.EncodeToString(iv) + ":" + hex.EncodeToString(ciphertext), nil
}


//decrypt decrypts ciphertext
func decrypt(key, ciphertext string) (string, error) {
   block, err := newCipherBlock(key)
   if err != nil {
      return "", err
   }

  //split ciphertext into initialisation vector and actual ciphertext
   ciphertextParts := strings.Split(ciphertext, ":")
   iv, err := hex.DecodeString(ciphertextParts[0])
   if err != nil {
      return "", err
   }
   ciphertextbs, err := hex.DecodeString(ciphertextParts[1])
   if err != nil {
      return "", err
   }

   if len(ciphertextParts[1]) < aes.BlockSize {
      return "", errors.New("ciphertext too short")
   }

   // CBC mode always works in whole blocks.
   if len(ciphertextParts[1])%aes.BlockSize != 0 {
      return "", errors.New("ciphertext is not a multiple of the block size")
   }

   mode := cipher.NewCBCDecrypter(block, iv)


   // Decrypt cipher text
   mode.CryptBlocks(ciphertextbs, ciphertextbs)

  // Unpad ciphertext
   ciphertextbs, err = pkcs7Unpad(ciphertextbs, aes.BlockSize)
   if err != nil{
      return "", err
   }

   return string(ciphertextbs), nil
}

And we can now test our functions like so:

func main() {
  pt := "Highly confidential message!"
  key := "aSecret"

   ct, err := encrypt(key, pt)
   if err != nil{
      log.Fatalln(err)
   }
   fmt.Println(ct)  //00af9595ed8bae4c443465aff651e4f6:a1ceea8703bd6aad969a64e7439d0664320bb2f73d9a31433946b81819cb0085

   ptt, err := decrypt(key, ct)
   if err != nil{
      log.Fatalln(err)
   }
   fmt.Println(ptt)  //Highly confidential message!

}

Public-key cryptography

Public-key cryptography is different from symmetric-key cryptography in that different keys are used for encryption and decryption. Two different keys exist: the private key used for decryption and the public key used for encryption.

RSA is a popular example of a public-key cryptosystem and can be implemented in Go using the rsa subpackage.

To implement RSA, we have to generate our private and public keys first. To do this, we can generate a private key using GenerateKey and then generate the public key from the private key.

func main(){
//create an RSA key pair of size 2048 bits
  priv, err := rsa.GenerateKey(rand.Reader, 2048)
  if err != nil{
     log.Fatalln(err)
  }

  pub := priv.Public()
}

We can now use RSA in combination with OAEP to encrypt and decrypt plaintext and ciphertext as we like.

func main(){
    ...
    options := rsa.OAEPOptions{
     crypto.SHA256,
     []byte("label"),
  }

  message := "Secret message!"

  rsact, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pub.(*rsa.PublicKey), []byte(message), options.Label)
  if err != nil{
     log.Fatalln(err)
  }

  fmt.Println("RSA ciphertext", hex.EncodeToString(rsact))

  rsapt, err := priv.Decrypt(rand.Reader,rsact, &options)
  if err != nil{
     log.Fatalln(err)
  }

  fmt.Println("RSA plaintext", string(rsapt))

}

Digital signatures

Digital signatures are another application of cryptography. Digital signatures basically allow us to verify that a message being transmitted across, say, a network has not been tampered with by an attacker.

A common method of implementing digital signatures is with Message Authentication codes (MACs), specifically HMAC. HMACs make use of hash functions and are a secure way to ensure authenticity of a message. We can implement HMACs in Go using the hmac subpackage.

Here’s an example of how it’s done:

/*hmacs make use of an underlying hash function so we have to specify one*/
mac := hmac.New(sha256.New, []byte("secret"))
mac.Write([]byte("Message whose authenticity we want to guarantee"))
macBS := mac.Sum(nil) //

falseMac := []byte("someFalseMacAsAnArrayOfBytes")
equal := hmac.Equal(falseMac, macBS)
fmt.Println(equal)  //false - therefore the message to which this hmac is attached has been tampered

Bcrypt

Aside from the Go standard crypto library, there are other cryptography-related packages in the Go ecosystem. One of these is bcrypt.

The bcrypt package is the Go implementation of the popular hashing algorithm bcrypt. Bcrypt is the industry-standard algorithm for hashing passwords, and most languages have some form of bcrypt implementation.

In this package, we can obtain bcrypt hashes from a password using the GenerateFromPassword function and passing in a cost.

hash, err := bcrypt.GenerateFromPassword("password", bcrypt.DefaultCost)

We can then check if a given password matches a given hash later on by doing:

err := bcrypt.CompareHashAndPassword([]byte("hashedPassword"), []byte("password"))

Conclusion

That’s it for this article! Hopefully, this article gave you an idea of how robust the Go ecosystem is, at least with respect to cryptography. You can also check out the contents of the Go standard library here to see what else comes baked in with Go.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Yusuff Faruq Frontend web developer and anime lover from Nigeria.

Leave a Reply