Search code examples
securityencryptiongoaeskdf

How secure is my encryption with password script? (Golang, AES256, pbkdf2, hmac)


First, I want to say that this is just an learning exercise and I do not intend to use this in production.

I wrote a small application in Golang with two functions: encrypt(plaintext string, password string) and decrypt(encrypted string, password string)

The encryption steps are:

  1. Generate random 256 bits to use as salt
  2. Generate 128 bits to use as an Initialization Vector
  3. Use PDKDF2 to generate a 32 bit key from the password and salt
  4. Generate an 32 bit HMAC with the key and plaintext, and append it to the beginning of the plaintext
  5. Encrypt the hmac+plaintext with AES in CFB mode

The returned byte array looks like this:

[256 bit salt] [128 bit iv] encrypted([256 bit hmac] [plaintext])

When decrypting:

  1. Extract the salt and use it with the provided password to compute the key
  2. Extract the IV and decrypt the encrypted portion of the ciphertext
  3. Extract the mac from the decrypted value
  4. Validate the mac with the plaintext

I'm not crazy enough to use my own encryption script in any production projects, so please point me to any libraries that do this for me (simple password / message encryption that is relatively secure)

Here is the source code to the two functions:

package main

import (
    "io"
    "crypto/rand"
    "crypto/cipher"
    "crypto/aes"
    "crypto/sha256"
    "crypto/hmac"
    "golang.org/x/crypto/pbkdf2"
)


const saltlen = 32
const keylen = 32
const iterations = 100002

// returns ciphertext of the following format:
// [32 bit salt][128 bit iv][encrypted plaintext]
func encrypt(plaintext string, password string) string {
    // allocate memory to hold the header of the ciphertext
    header := make([]byte, saltlen + aes.BlockSize)

    // generate salt
    salt := header[:saltlen]
    if _, err := io.ReadFull(rand.Reader, salt); err != nil {
        panic(err)
    }

    // generate initialization vector
    iv := header[saltlen:aes.BlockSize+saltlen]
    if _, err := io.ReadFull(rand.Reader, iv); err != nil {
        panic(err)
    }

    // generate a 32 bit key with the provided password
    key := pbkdf2.Key([]byte(password), salt, iterations, keylen, sha256.New)

    // generate a hmac for the message with the key
    mac := hmac.New(sha256.New, key)
    mac.Write([]byte(plaintext))
    hmac := mac.Sum(nil)

    // append this hmac to the plaintext
    plaintext = string(hmac) + plaintext

    //create the cipher
    block, err := aes.NewCipher(key)
    if err != nil {
        panic(err)
    }

    // allocate space for the ciphertext and write the header to it
    ciphertext := make([]byte, len(header) + len(plaintext))
    copy(ciphertext, header)

    // encrypt
    stream := cipher.NewCFBEncrypter(block, iv)
    stream.XORKeyStream(ciphertext[aes.BlockSize+saltlen:], []byte(plaintext))
    return string(ciphertext)
}

func decrypt(encrypted string, password string) string {
    ciphertext := []byte(encrypted)
    // get the salt from the ciphertext
    salt := ciphertext[:saltlen]
    // get the IV from the ciphertext
    iv := ciphertext[saltlen:aes.BlockSize+saltlen]
    // generate the key with the KDF
    key := pbkdf2.Key([]byte(password), salt, iterations, keylen, sha256.New)

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

    if len(ciphertext) < aes.BlockSize {
        return ""
    }

    decrypted := ciphertext[saltlen+aes.BlockSize:]
    stream := cipher.NewCFBDecrypter(block, iv)
    stream.XORKeyStream(decrypted, decrypted)

    // extract hmac from plaintext
    extractedMac := decrypted[:32]
    plaintext := decrypted[32:]

    // validate the hmac
    mac := hmac.New(sha256.New, key)
    mac.Write(plaintext)
    expectedMac := mac.Sum(nil)
    if !hmac.Equal(extractedMac, expectedMac) {
        return ""
    }

    return string(plaintext)
}

Solution

  • Note, since the question was about encrypting messages rather than passwords: If you're encrypting small messages rather than hashing passwords, Go's secretbox package—as part of its NaCl implementation—is the way to go. If you're intent on rolling your own—and I strongly recommend against it, unless it stays within your own dev environment—then AES-GCM is the way to go here.

    Otherwise, most of the below still applies:

    1. Symmetric encryption isn't useful for passwords. There should be no reason why you need the plaintext back—you should only care about comparing hashes (or, more precisely, derivative keys).
    2. PBKDF2, compared to scrypt or bcrypt, is not ideal (10002 rounds, in 2015, is probably a bit low too). scrypt is memory-hard and much harder to parallelize on a GPU, and in 2015, has had a sufficiently long life as to make it safer than bcrypt (you would still use bcrypt in cases where the scrypt library for your language wasn't great).
    3. MAC-then-encrypt has issues - you should encrypt-then-MAC.
    4. Given #3, you should use AES-GCM (Galois Counter Mode) over AES-CBC + HMAC.

    Go has a great bcrypt package with an easy-to-use API (generates salts for you; securely compares).

    I also wrote an scrypt package that mirrors that package, as the underlying scrypt package requires you to validate your own params and generate your own salts.