$Id: 307

$SOId: 30884

You have a file and a password and you want to encrypt the file with a password.

There are many encryption algorithms.

This chapter describes how to use symmetric algorithm AES (Advanced Encryption Standard) in GCM mode.

GCM mode provides both encryption and authentication.

Without authentication an attacker could change encrypted bytes which would result in successful decryption but corrupted data. By adding authentication, GCM mode detects that encrypted data were corrupted.

Symmetric means that we can use the same password to both encrypt and decrypt the data.

AES uses 16 bytes key as a password. Humans prefer passwords of arbitrary length.

To support humans we need to derive AES key from a human password. This is harder than it looks so you should use one of the methods that are well researched and considered cryptographically secure. One of those methods is scrypt key derivation function.

package main

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"

	"golang.org/x/crypto/scrypt"
)

func aesKeyFromPassword(password string) ([]byte, error) {
	// DO NOT use this salt value; generate your own random salt. 8 bytes is
	// a good length. Keep the salt secret.
	secretSalt := []byte{0xbc, 0x1e, 0x07, 0xd7, 0xb2, 0xa2, 0x5e, 0x2c}
	return scrypt.Key([]byte(password), secretSalt, 32768, 8, 1, 32)
}

func aesGcmEncrypt(unencrypted []byte, password string) ([]byte, error) {
	key, err := aesKeyFromPassword(password)
	if err != nil {
		return nil, err
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

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

	// generate a random nonce (makes encryption stronger)
	nonce := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, err
	}

	encrypted := gcm.Seal(nil, nonce, unencrypted, nil)
	// we need nonce for decryption so we put it at the beginning
	// of encrypted text
	return append(nonce, encrypted...), nil
}

func aesGcmDecrypt(encrypted []byte, password string) ([]byte, error) {
	key, err := aesKeyFromPassword(password)
	if err != nil {
		return nil, err
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

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

	if len(encrypted) < gcm.NonceSize() {
		return nil, errors.New("Invalid data")
	}

	// extract random nonce we added to the beginning of the file
	nonce := encrypted[:gcm.NonceSize()]
	encrypted = encrypted[gcm.NonceSize():]

	return gcm.Open(nil, nonce, encrypted, nil)
}

func main() {
	password := "my password"
	d, err := ioutil.ReadFile("main.go")
	if err != nil {
		log.Fatalf("ioutil.ReadFile() failed with %s\\n", err)
	}
	encrypted, err := aesGcmEncrypt(d, password)
	if err != nil {
		log.Fatalf("aesGcmEncrypt() failed with %s\\n", err)
	}
	decrypted, err := aesGcmDecrypt(encrypted, password)
	if err != nil {
		log.Fatalf("aesGcmDecrypt() failed with %s\\n", err)
	}
	if !bytes.Equal(d, decrypted) {
		log.Fatalf("decryption created data different than original\\n")
	} else {
		fmt.Printf("Encryption in decryption worked!\\n")
	}
}

Encryption is a tricky subject and it's easy to make a mistake that would make it easy for the attacker to break encryption and decrypt the file.

It's really important to convert a human-readable password to a random encryption key.

Humans tend to use only a subset of possible bytes for passwords which makes them much easier to break.

Scrypt is considered a good algorithm for generating encryption key from a human password. As you can see it also uses a salt value which you should keep secret.

There are several variants of AES algorithms. We chose GCM because it combines authentication with encryption. Authentication detects modification of encrypted data.

To make encryption stronger GCM modes requires additional random bytes. We chose to generate unique nonce for each file and store it at the beginning of encrypted data (the nonce doesn't have to be secret).

An alternative would be to generate only one nonce and use it for all files.