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.
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 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:
h := md5.New()
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 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 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
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"))
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Angular’s new `defer` feature, introduced in Angular 17, can help us optimize the delivery of our apps to end users.
ElectricSQL is a cool piece of software with immense potential. It gives developers the ability to build a true local-first application.
Leptos is an amazing Rust web frontend framework that makes it easier to build scalable, performant apps with beautiful, declarative UIs.
Learn more about the 5 best JavaScript libraries for dealing with multidimensional arrays, such as ndarray, math.js, and NumJs.