Ugrás a fő tartalomhoz

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 source1. 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 hashing2

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.


Footnotes

  1. Rainbow table attacks are not a weakness on the hashing algorithms.

  2. Consider reading the Authentication and Password Management section about "strong one-way salted hashes" for credentials.