Encrypting and Signing Using libgcrypt

An understanding of cryptographic primitives prior to beginning this tutorial is recommended.

A while back, I coded up an example of how to use the libgcrypt C library. Given an input file, an output file, and a passphrase, the application can encrypt or decrypt the input file.

This application is not suitable for production use. It loads the entire file into memory prior to encryption/decryption. A much more efficient implementation would process pieces of the data iteratively.

The source code is pretty well commented but I want to walk through it in this tutorial.

Important #defines

Let’s take a look at the encrypt_file function; it makes for a natural starting point. Take note of the #defines:

#define AES256_KEY_SIZE     32
#define AES256_BLOCK_SIZE   16
#define HMAC_KEY_SIZE       64
#define KDF_ITERATIONS      50000
#define KDF_SALT_SIZE       128
#define KDF_KEY_SIZE        AES256_KEY_SIZE + HMAC_KEY_SIZE

The first two defines add clarity and help us avoid magic numbers. HMAC_KEY_SIZE is the size of the key that we will generate using PBKDF2.

KDF_ITERATIONS is the number of PBKDF2 iterations to perform. KDF_SALT_SIZE is the size of the PBKDF2 salt and KDF_KEY_SIZE is the size in bytes of the key produced by PBKDF2. More on all that in the next section.

Preparing for Encryption/Decryption

Key Derivation

PBKDF2 turns the plaintext passphrase into a cryptographically secure key of a specific size. One benefit of using a key derivation function like PBKDF2 is that we can increase the number of hashing iterations it performs while deriving keys, causing the algorithm to run slower. Anywhere from 10,000 to 100,000 iterations is generally acceptable at the time of this writing. A counterintuitive advantage, this makes our key and passphrase resistant to offline brute force attacks. A 128 byte salt for the KDF (Key Derivation Function; PBKDF2, specifically) is unnecessarily gigantic but it certainly doesn’t hurt.

Since we are using AES 256, we will need a 256 bit (32 byte) key to perform encryption with. We will use the lower 32 bytes of the key we generated from our passphrase using PBKDF2 as the 256 bit AES key. We will use the higher 32 bytes as our HMAC private signing key.

After we read the file into memory and compute some necessary sizes, we generate a salt for the key derivation and then perform the key derivation. The resulting KDF_KEY_SIZE byte key will be stored in the kdf_key variable:

// Generate a KDF_SALT_SIZE byte salt in preparation for key derivation
gcry_create_nonce(kdf_salt, KDF_SALT_SIZE);

// Key derivation: PBKDF2 using SHA512 w/ KDF_SALT_SIZE byte salt over KDF_ITERATIONS iterations into a KDF_KEY_SIZE byte key
err = gcry_kdf_derive(password,
                      strlen(password),
                      GCRY_KDF_PBKDF2,
                      GCRY_MD_SHA512,
                      kdf_salt,
                      KDF_SALT_SIZE,
                      KDF_ITERATIONS,
                      KDF_KEY_SIZE,
                      kdf_key);

After we derive a key from our passphrase using PBKDF2, we have a couple memcpy()‘s setting up our aes_key and hmac_key buffers.

Generating the Initialization Vector and Initializing the Cipher Handle

Once our encryption key (aes_key) and our HMAC private signing key (hmac_key) are derived, we see:

// Generate the initialization vector
gcry_create_nonce(init_vector, AES256_BLOCK_SIZE);

// Begin encryption
if (init_cipher(&handle, aes_key, init_vector)) {
  free(text);
  return 1;
}

We create an initialization vector (“IV”) by using the gcrypt function for generating a nonce; according to the gcrypt documentation, the gcry_create_nonce function is appropriate for generating IVs. Initialization vectors prevent attackers from gleaning information from many different ciphertexts that were all encrypted using the same key.

After we have our encryption key ready, our initialization vector generated, and our gcry_cipher_hd_t handle variable declared, we are prepared to invoke our init_cipher helper function. The gcry_cipher_setkey() and gcry_cipher_setiv() functions set our encryption key and initialization vector in preparation to execute the encryption:

int init_cipher (gcry_cipher_hd_t *handle, unsigned char *key, unsigned char *init_vector) {
  gcry_error_t err;

  // 256-bit AES using cipher-block chaining; with ciphertext stealing, no manual padding is required
  err = gcry_cipher_open(handle,
                         GCRY_CIPHER_AES256,
                         GCRY_CIPHER_MODE_CBC,
                         GCRY_CIPHER_CBC_CTS);
  if (err) {
    fprintf(stderr, "cipher_open: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
    return 1;
  }

  err = gcry_cipher_setkey(*handle, key, AES256_KEY_SIZE);
  if (err) {
    fprintf(stderr, "cipher_setkey: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
    gcry_cipher_close(*handle);
    return 1;
  }

  err = gcry_cipher_setiv(*handle, init_vector, AES256_BLOCK_SIZE);
  if (err) {
    fprintf(stderr, "cipher_setiv: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
    gcry_cipher_close(*handle);
    return 1;
  }

  return 0;
}

This function instantiates the gcrypt cipher handle object. This object is going to store a pointer to the data we want to encrypt, a bunch of metadata specifying the algorithm we want to use, modes, and other algorithm-specific configuration information, our private keys, and a pointer to the buffer allocated to store the ciphertext.

Encrypt the Data

After we allocate space for the ciphertext buffer, we perform the encryption by invoking gcry_cipher_encrypt(). The encryption is performed in-place so we only need one buffer to hold the plaintext and the ciphertext:

// Make new buffer of size blocks_required * AES256_BLOCK_SIZE for in-place encryption
ciphertext = malloc(blocks_required * AES256_BLOCK_SIZE);
if (ciphertext == NULL) {
  fprintf(stderr, "Error: unable to allocate memory for the ciphertext\n");
  cleanup(handle, NULL, text, NULL, NULL);
  return 1;
}
memcpy(ciphertext, text, blocks_required * AES256_BLOCK_SIZE);
free(text);

// Encryption is performed in-place
err = gcry_cipher_encrypt(handle, ciphertext, AES256_BLOCK_SIZE * blocks_required, NULL, 0);
if (err) {
  fprintf(stderr, "cipher_encrypt: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
  cleanup(handle, NULL, ciphertext, NULL, NULL);
  return 1;
}

Packing the Encrypted Data

Once the file has been encrypted, let’s pack it into a standardized format that we can easily unpack later:

// Compute and allocate space required for packed data
hmac_len = gcry_mac_get_algo_maclen(GCRY_MAC_HMAC_SHA512);
packed_data_len = KDF_SALT_SIZE + AES256_BLOCK_SIZE + (AES256_BLOCK_SIZE * blocks_required) + hmac_len;
packed_data = malloc(packed_data_len);
if (packed_data == NULL) {
  fprintf(stderr, "Unable to allocate memory for packed data\n");
  cleanup(handle, NULL, ciphertext, NULL, NULL);
  return 1;
}

// Pack data before writing: salt::IV::ciphertext::HMAC where "::" denotes concatenation
memcpy(packed_data, kdf_salt, KDF_SALT_SIZE);
memcpy(&(packed_data[KDF_SALT_SIZE]), init_vector, AES256_BLOCK_SIZE);
memcpy(&(packed_data[KDF_SALT_SIZE + AES256_BLOCK_SIZE]), ciphertext, AES256_BLOCK_SIZE * blocks_required);

The comment tells us that the data is packed in the following order: PBKDF2 salt comes first, AES initialivation vector comes second, ciphertext comes third, and the signature (HMAC) over the aforementioned 3 components. In the code block above, we are packing the salt, IV, and ciphertext without the HMAC in preparation for signing.

The ordering of the salt, IV, and ciphertext while performing the HMAC is not significant. What is significant, however, is that all three of those components are included in the HMAC. Putting the ciphertext after the salt and the IV also makes it easier to read, encrypt, and write data piece by piece. Putting the HMAC at the end makes sense only because it is generated and packed in last.

Signing the Packed Data

Let’s compute the HMAC over the packed KDF salt, IV, and ciphertext:

// Begin HMAC computation on encrypted/packed data
hmac = malloc(hmac_len);
if (hmac == NULL) {
  fprintf(stderr, "Error: unable to allocate enough memory for the HMAC\n");
  cleanup(handle, NULL, ciphertext, packed_data, NULL);
  return 1;
}

err = gcry_mac_open(&mac, GCRY_MAC_HMAC_SHA512, 0, NULL);
if (err) {
  fprintf(stderr, "mac_open during encryption: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
  cleanup(handle, NULL, ciphertext, packed_data, hmac);
  return 1;
}

err = gcry_mac_setkey(mac, hmac_key, HMAC_KEY_SIZE);
if (err) {
  fprintf(stderr, "mac_setkey during encryption: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
  cleanup(handle, mac, ciphertext, packed_data, hmac);
  return 1;
}

// Add packed_data to the MAC computation
err = gcry_mac_write(mac, packed_data, packed_data_len - hmac_len);
if (err) {
  fprintf(stderr, "mac_write during encryption: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
  cleanup(handle, mac, ciphertext, packed_data, hmac);
  return 1;
}

// Finalize MAC and save it in the hmac buffer
err = gcry_mac_read(mac, hmac, &hmac_len);
if (err) {
  fprintf(stderr, "mac_read during encryption: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
  cleanup(handle, mac, ciphertext, packed_data, hmac);
  return 1;
}

We allocate space for the HMAC, initialize the MAC struct provided by gcrypt, set the private signing key to be used for the HMAC, add the packed data (salt, IV, ciphertext) to the HMAC, and then compute the HMAC which is then written to the hmac buffer by libgcrypt.

Add the Signature to the Packed Data

Finally, we add the computed HMAC to the packed data for delivery to the recipient, write the packed data to disk, and clean up after ourselves:

// Append the computed HMAC to packed_data
memcpy(&(packed_data[KDF_SALT_SIZE + AES256_BLOCK_SIZE + (AES256_BLOCK_SIZE * blocks_required)]), hmac, hmac_len);

// Write packed data to file
if (!write_buf_to_file(outfile, packed_data, packed_data_len)) {
  cleanup(handle, mac, ciphertext, packed_data, hmac);
  return 1;
}

cleanup(handle, mac, ciphertext, packed_data, hmac);

return 0;

There you have it! Your file is now encrypted using AES-256 with integrity checking and authenticity verification handled by HMAC-SHA512.

Decrypt the Data

When we want to decrypt a file that we have encrypted, we need to compute space requirements, allocate space for all the packed data, and unpack the data before we can begin operating on it:

// Read in file contents
if (!(packed_data_len = read_file_into_buf(infile, &packed_data))) {
  return 1;
}

// Compute necessary lengths
hmac_len = gcry_mac_get_algo_maclen(GCRY_MAC_HMAC_SHA512);
ciphertext_len = packed_data_len - KDF_SALT_SIZE - AES256_BLOCK_SIZE - hmac_len;

ciphertext = malloc(ciphertext_len);
if (ciphertext == NULL) {
  fprintf(stderr, "Error: ciphertext is too large to fit in memory\n");
  free(packed_data);
  return 1;
}

hmac = malloc(hmac_len);
if (hmac == NULL) {
  fprintf(stderr, "Error: could not allocate memory for HMAC\n");
  cleanup(NULL, NULL, ciphertext, packed_data, NULL);
  return 1;
}

// Unpack data
memcpy(kdf_salt, packed_data, KDF_SALT_SIZE);
memcpy(init_vector, &(packed_data[KDF_SALT_SIZE]), AES256_BLOCK_SIZE);
memcpy(ciphertext, &(packed_data[KDF_SALT_SIZE + AES256_BLOCK_SIZE]), ciphertext_len);
memcpy(hmac, &(packed_data[KDF_SALT_SIZE + AES256_BLOCK_SIZE + ciphertext_len]), hmac_len);

First, we read our file into memory. We then compute the size of the data so we know how much space to allocate. Once we have allocated the required space, we unpack the data in the buffers we have allocated in preparation for decryption.

Key Derivation

Just like with encryption, we need to derive the AES key and the HMAC key used to encrypt and sign the data. If the user provides the correct passphrase, the AES key and HMAC key generated here will be identical to the keys generated by PBKDF2 during the encryption process:

// Key derivation: PBKDF2 using SHA512 w/ KDF_SALT_SIZE byte salt over KDF_ITERATIONS iterations into a KDF_KEY_SIZE byte key
err = gcry_kdf_derive(password,
                      strlen(password),
                      GCRY_KDF_PBKDF2,
                      GCRY_MD_SHA512,
                      kdf_salt,
                      KDF_SALT_SIZE,
                      KDF_ITERATIONS,
                      KDF_KEY_SIZE,
                      kdf_key);
if (err) {
  fprintf(stderr, "kdf_derive: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
  cleanup(NULL, NULL, ciphertext, packed_data, hmac);
  return 1;
}

// Copy the first AES256_KEY_SIZE bytes of kdf_key into aes_key
memcpy(aes_key, kdf_key, AES256_KEY_SIZE);

// Copy the last HMAC_KEY_SIZE bytes of kdf_key into hmac_key
memcpy(hmac_key, &(kdf_key[AES256_KEY_SIZE]), HMAC_KEY_SIZE);

Check the Signature

Prior to attempting decryption, we need to check the signature.

It is absolutely critical that we check the signature before proceeding with decryption. If the signature does not check out, we MUST NOT attempt decryption. To do so would make us vulnerable to chosen-ciphertext attacks:

// Begin HMAC verification
err = gcry_mac_open(&mac, GCRY_MAC_HMAC_SHA512, 0, NULL);
if (err) {
  fprintf(stderr, "mac_open during decryption: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
  cleanup(handle, NULL, ciphertext, packed_data, hmac);
  return 1;
}

err = gcry_mac_setkey(mac, hmac_key, HMAC_KEY_SIZE);
if (err) {
  fprintf(stderr, "mac_setkey during decryption: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
  cleanup(handle, mac, ciphertext, packed_data, hmac);
  return 1;
}

err = gcry_mac_write(mac, packed_data, KDF_SALT_SIZE + AES256_BLOCK_SIZE + ciphertext_len);
if (err) {
  fprintf(stderr, "mac_write during decryption: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
  cleanup(handle, mac, ciphertext, packed_data, hmac);
  return 1;
}

// Verify HMAC
err = gcry_mac_verify(mac, hmac, hmac_len);
if (err) {
  fprintf(stderr, "HMAC verification failed: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
  cleanup(handle, mac, ciphertext, packed_data, hmac);
  return 1;
} else {
  printf("Valid HMAC found\n");
}

First, we open the MAC handle, set the HMAC key we derived from the passphrase using PBKDF2, write the packed version of the KDF salt, AES IV, and ciphertext to the HMAC, and then invoke the verification function provided by libgcrypt.

In the presence of an invalid signature, we free all of our allocated resources and abort decryption. No exceptions.

In the event the signature checks out, we proceed with the decryption:

// Begin decryption
if (init_cipher(&handle, aes_key, init_vector)) {
  cleanup(handle, mac, ciphertext, packed_data, hmac);
  return 1;
}

err = gcry_cipher_decrypt(handle, ciphertext, ciphertext_len, NULL, 0);
if (err) {
  fprintf(stderr, "cipher_decrypt: %s/%s\n", gcry_strsource(err), gcry_strerror(err));
  cleanup(handle, mac, ciphertext, packed_data, hmac);
  return 1;
}

// Write plaintext to the output file
if (!write_buf_to_file(outfile, ciphertext, ciphertext_len)) {
  fprintf(stderr, "0 bytes written.\n");
}

cleanup(handle, mac, ciphertext, packed_data, hmac);

return 0;

And there we have it. Assuming our signature checks out - that is, assuming the proper passphrase was provided by the user AND neither the file payload nor the signature were modified since it was packed - we have now recovered our original plaintext!