Cryptographic Practices

Let’s make the first statement as strong as your cryptography should be: hashing and encrypting are two different things.

There’s a general misconception, and most of the time, hashing and encrypting are used interchangeably, in an incorrect way. They are different concepts, and they also serve different purposes.

A hash is a string or number generated by a (hash) function from source data:

hash := F(data)

The hash has a fixed length and its value vary widely with small variations in input (collisions may still happen). A good hashing algorithm won’t allow a hash to turn into its original source. MD5 is the most popular hashing algorithm, but securitywise BLAKE2 is considered the strongest and most flexible.

Go supplementary cryptography libraries offers both BLAKE2b (or just BLAKE2) and BLAKE2s implementations: the former is optimized for 64-bit platforms and the latter for 8-bit to 32-bit platforms. If BLAKE2 is unavailable, SHA-256 is the right option.

Please note that slowness is something desired on a cryptographic hashing algorithm. Computers become faster over time, meaning that attacker can try more and more potential passwords as years pass (see Credential Stuffing and Brute-force attacks). To fight back, the hashing function should be inherently slow, using at least 10,000 iterations.

Whenever you have something that you don’t need to know what it is, but only if it’s what it’s supposed to be (like checking file integrity after download), you should use hashing1

package main

import "fmt"
import "io"
import "crypto/md5"
import "crypto/sha256"
import "golang.org/x/crypto/blake2s"

func main () {
        h_md5 := md5.New()
        h_sha := sha256.New()
        h_blake2s, _ := blake2s.New256(nil)
        io.WriteString(h_md5, "Welcome to Go Language Secure Coding Practices")
        io.WriteString(h_sha, "Welcome to Go Language Secure Coding Practices")
        io.WriteString(h_blake2s, "Welcome to Go Language Secure Coding Practices")
        fmt.Printf("MD5        : %x\n", h_md5.Sum(nil))
        fmt.Printf("SHA256     : %x\n", h_sha.Sum(nil))
        fmt.Printf("Blake2s-256: %x\n", h_blake2s.Sum(nil))
}

The output

MD5        : ea9321d8fb0ec6623319e49a634aad92
SHA256     : ba4939528707d791242d1af175e580c584dc0681af8be2a4604a526e864449f6
Blake2s-256: 1d65fa02df8a149c245e5854d980b38855fd2c78f2924ace9b64e8b21b3f2f82

Note: To run the source code sample you’ll need to run $ go get golang.org/x/crypto/blake2s

On the other hand, encryption turns data into variable length data using a key

encrypted_data := F(data, key)

Unlike the hash, we can compute data back from encrypted_data by applying the right decryption function and key:

data := F⁻¹(encrypted_data, key)

Encryption should be used whenever you need to communicate or store sensitive data, which you or someone else needs to access later on for further processing. A “simple” encryption use case is the HTTPS - Hyper Text Transfer Protocol Secure. AES is the de facto standard when it comes to symmetric key encryption. This algorithm, similar to many other symmetric ciphers, can be implemented in different modes. You’ll notice in the code sample below, GCM (Galois Counter Mode) was used, instead of the more popular (in cryptography code examples, at least) CBC/ECB. The main difference between GCM and CBC/ECB is the fact that the former is an authenticated cipher mode, meaning that after the encryption stage, an authentication tag is added to the ciphertext, which will then be validated prior to message decryption, ensuring the message has not been tampered with. In comparison, you have Public key cryptography or asymmetric cryptography which makes use of pairs of keys: public and private. Public key cryptography offers less performance than symmetric key cryptography for most cases. Therefore, its most common use-case is sharing a symmetric key between two parties using asymmetric cryptography, so they can then use the symmetric key to exchange messages encrypted with symmetric cryptography. Aside from AES, which is 1990’s technology, Go authors have begun to implement and support more modern symmetric encryption algorithms, which also provide authentication, for example, chacha20poly1305.

Another interesting package in Go is x/crypto/nacl. This is a reference to Dr. Daniel J. Bernstein’s NaCl library, which is a very popular modern cryptography library. The nacl/box and nacl/secretbox in Go are implementations of NaCl’s abstractions for sending encrypted messages for the two most common use-cases:

  • Sending authenticated, encrypted messages between two parties using public key cryptography (nacl/box)
  • Sending authenticated, encrypted messages between two parties using symmetric (a.k.a secret-key) cryptography

It is very advisable to use one of these abstractions instead of direct use of AES, if they fit your use-case.

The example below illustrates encryption and decryption using an AES-256 (256 bits/32 bytes) based key, with a clear separation of concerns such as encryption, decryption and secret generation. The secret method is a convenient option which helps generate a secret. The source code sample was taken from here but has been slightly modified.

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "io"
    "log"
)

func encrypt(val []byte, secret []byte) ([]byte, error) {
    block, err := aes.NewCipher(secret)
    if err != nil {
        return nil, err
    }

    aead, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }

    nonce := make([]byte, aead.NonceSize())
    if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
        return nil, err
    }

    return aead.Seal(nonce, nonce, val, nil), nil
}

func decrypt(val []byte, secret []byte) ([]byte, error) {
    block, err := aes.NewCipher(secret)
    if err != nil {
        return nil, err
    }

    aead, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }

    size := aead.NonceSize()
    if len(val) < size {
        return nil, err
    }

    result, err := aead.Open(nil, val[:size], val[size:], nil)
    if err != nil {
        return nil, err
    }

    return result, nil
}

func secret() ([]byte, error) {
    key := make([]byte, 16)

    if _, err := rand.Read(key); err != nil {
        return nil, err
    }

    return key, nil
}

func main() {
    secret, err := secret()
    if err != nil {
        log.Fatalf("unable to create secret key: %v", err)
    }

    message := []byte("Welcome to Go Language Secure Coding Practices")
    log.Printf("Message  : %s\n", message)

    encrypted, err := encrypt(message, secret)
    if err != nil {
        log.Fatalf("unable to encrypt the data: %v", err)
    }
    log.Printf("Encrypted: %x\n", encrypted)

    decrypted, err := decrypt(encrypted, secret)
    if err != nil {
        log.Fatalf("unable to decrypt the data: %v", err)
    }
    log.Printf("Decrypted: %s\n", decrypted)
}
Message  : Welcome to Go Language Secure Coding Practices
Encrypted: b46fcd10657f3c269844da5f824511a0e3da987211bc23e82a9c050a2be287f51bb41dd3546742442498ae9fcad2ce40d88625d1840c11096a55cb4f217382befbeb636e479cfecfcd3a
Decrypted: Welcome to Go Language Secure Coding Practices

Please note, you should “establish and utilize a policy and process for how cryptographic keys will be managed”, protecting “master secrets from unauthorized access”. That being said, your cryptographic keys shouldn’t be hardcoded in the source code (as it is in this example).

Go’s crypto package collects common cryptographic constants, but implementations have their own packages, like the crypto/md5 one.

Most modern cryptographic algorithms have been implemented under https://godoc.org/golang.org/x/crypto, so developers should focus on those instead of the implementations in the crypto/* package.


Pseudo-Random Generators

In OWASP Secure Coding Practices you’ll find what seems to be a really complex guideline: “All random numbers, random file names, random GUIDs, and random strings should be generated using the cryptographic module’s approved random number generator when these random values are intended to be un-guessable”, so let’s discuss “random numbers”.

Cryptography relies on some randomness, but for the sake of correctness, what most programming languages provide out-of-the-box is a pseudo-random number generator: for example, Go’s math/rand is not an exception.

You should carefully read the documentation when it states that “Top-level functions, such as Float64 and Int, use a default shared Source that produces a deterministic sequence of values each time a program is run.” (source)

What exactly does that mean? Let’s see:

package main

import "fmt"
import "math/rand"

func main() {
    fmt.Println("Random Number: ", rand.Intn(1984))
}

Running this program several times will lead exactly to the same number/sequence, but why?

$ for i in {1..5}; do go run rand.go; done
Random Number:  1825
Random Number:  1825
Random Number:  1825
Random Number:  1825
Random Number:  1825

Because Go’s math/rand is a deterministic pseudo-random number generator. Similar to many others, it uses a source, called a Seed. This Seed is solely responsible for the randomness of the deterministic pseudo-random number generator. If it is known or predictable, the same will happen to generated number sequence.

We could “fix” this example quite easily by using the math/rand Seed function, getting the expected five different values for each program execution. But because we’re on Cryptographic Practices section, we should follow to Go’s crypto/rand package.

package main

import "fmt"
import "math/big"
import "crypto/rand"

func main() {
    rand, err := rand.Int(rand.Reader, big.NewInt(1984))
    if err != nil {
        panic(err)
    }

    fmt.Printf("Random Number: %d\n", rand)
}

You may notice that running crypto/rand is slower than math/rand, but this is expected since the fastest algorithm isn’t always the safest. Crypto’s rand is also safer to implement. An example of this is the fact that you CANNOT seed crypto/rand, since the library uses OS-randomness for this, preventing developer misuse.

$ for i in {1..5}; do go run rand-safe.go; done
Random Number: 277
Random Number: 1572
Random Number: 1793
Random Number: 1328
Random Number: 1378

If you’re curious about how this can be exploited just think what happens if your application creates a default password on user signup, by computing the hash of a pseudo-random number generated with Go’s math/rand, as shown in the first example.

Yes, you guessed it, you would be able to predict the user’s password!

  1. Consider reading the Authentication and Password Management section about “strong one-way salted hashes” for credentials. 


Licenses and Attributions


Speak Your Mind

-->