Exploring cryptsetup key generation

When playing with Linux device mapper trying to mount an encrypted file I naturally started utilizing cryptsetup. I new the passphrase that was used to encrypt the file and managed to mount it correctly, but checking the mapped device table info with '--showkey' option got me thinging: Knowing the passphrase, could I manage to do without cryptsetup and mount the volume directrly, only specyfing the 'table' parameters?

Let's start from the begining.

Linux device mapper is a framework that uses so called 'targets' to map one or more block devices into another block device. This mapping can be done in various, useful ways. For example it's possible to strip multiple disks into one large virtual drive. Or mirror them for data redundancy. If you want to get access to SAS drive or iSCSI disk via multipole routes (giving you extra performance, load balancing and failover functionality), you would use multipathing target. But what's important for this post is the ability to map encrypted drives. Let's see how it works, but instead of using physical drive, I'll just use a file.

First step is to create a temporary file, mount it as a loop device, then map it with cryptsetup:

$ file=$(mktemp)
$ dd if=/dev/zero of=$file bs=1M count=100
$ loop=$(sudo losetup -f)
$ sudo losetup $loop $file
$ sudo cryptsetup open $loop encrypted_file --type plain
Enter passphrase for /tmp/tmp.8zTuPQ8mcs: password
$ ls -al /dev/mapper
lrwxrwxrwx  1 root root       7 Jul 16 18:20 encrypted_file -> ../dm-0

With this we can create a filesystem on an encrypted device and finally mount it and create an example file:

$ sudo mkfs.ext4 $loop
$ mke2fs 1.46.5 (30-Dec-2021)
/dev/mapper/encrytped_file contains `OpenPGP Public Key' data
Proceed anyway? (y,N) y
$ mkdir /tmp/tmp_mount
$ sudo mount /dev/mapper/encrytped_file /tmp/tmp_mount
$ sudo touch /tmp/tmp_mount/file

One thing that baffled me was why would mkfs.ext4 report about 'OpenPGP Public Key'. I looked into it and it turns out, that mke2fs uses libmagic to check the formatted device for known data signatures. It just so happens, that mapping zeroed file using 'password' as a key, result in the first two bytes being '9a9d', which libmagic recognizes as PGP Public Key. It is however just a pure coincidence :)

Anyway, let's examine the table of our mapped device

$ sudo dmsetup table /dev/mapper/encrytped_file --showkey
$ 0 204800 crypt aes-cbc-essiv:sha256 2c08e8f5884750a7b99f6f2f342fc638db25ff31fb00ff3460badc65d7758851 0 7:0 0

Using this info it's possible to skip cryptsetup and map the encrytped block device only providing this data. But what if we didn't have this this info and knowing the encryption type, wanted to recreate it? All this data can be deduced, except of course for the key. But knowing the password it should be possible to recreate it. Let's check the --help page of cryptsetup to guide as:

$ cryptsetup --help
Default compiled-in device cipher parameters:
      loop-AES: aes, Key 256 bits
      plain: aes-cbc-essiv:sha256, Key: 256 bits, Password hashing: ripemd160

Oh, it looks that the key is a hash of our password. Let's check that:

$ echo -n password |openssl dgst --ripemd160
$ (stdin)= 2c08e8f5884750a7b99f6f2f342fc638db25ff31

The beginning is the same, but it lacks some characters at the end. Manual is of little help in this case: "The hash result will be truncated to the key size of the used cipher, or the size specified with -s."

Looks like some code check is necessary here. I cloned this gitlab version https://gitlab.com/cryptsetup/cryptsetup/-/tree/1a55b69a0f8028150a9c93455f24617bc7c8bd61 and started from the beginning.

Cryptsetup starts in src/cryptsetup.c. After parsing arguments and preparing everything, this start the specified action:

r = run_action(action);

run_action then invokes handler method of action struct.

r = action->handler();
static struct action_type {
    const char *type;
    int (*handler)(void);
    const char *(*verify)(void);
    int required_action_argc;
    int required_memlock;
    const char *arg_desc;
    const char *desc;
    } action_types[] = {
    { OPEN_ACTION,            action_open,            verify_open,            1, 1, N_("<device> [--type <type>] [<name>]"),N_("open device as <name>") },
    (...)

This handler is an action_open function, which in turn calls action_open_plain.

      } else if (!strcmp(device_type, "plain")) {
              if (action_argc < 2 && !ARG_SET(OPT_REFRESH_ID))
                      goto out;
              return action_open_plain();

In here the program asks user for the password:

r = tools_get_key(NULL, &password, &passwordLen,

and passes it to crypt_activate_by_passphrase function:

r = crypt_activate_by_passphrase(cd, activated_name,
      CRYPT_ANY_SLOT, password, passwordLen, activate_flags);

which is defined in lib/setup.c. From this function we are sent to _activate_by_passphrase:

return _activate_by_passphrase(cd, name, keyslot, passphrase, passphrase_size, flags);

Next stop is process_key function:

r = process_key(cd, cd->u.plain.hdr.hash,
              cd->u.plain.key_size,
              passphrase, passphrase_size, &vk);

that kicks as to crypt_plain_hash function:

r = crypt_plain_hash(cd, hash_name, (*vk)->key, key_size, pass, passLen);

This function is implemented in lib/crypt_plain.c and send us to our final destination, which is the function hash:

r = hash(hash_name_buf, hash_size, key, passphrase_size, passphrase);

hash is just above crypt_plain_hash and looks like this:

static int hash(const char *hash_name, size_t key_size, char *key,
                size_t passphrase_size, const char *passphrase)
{
        struct crypt_hash *md = NULL;
        size_t len;
        int round, i, r = 0;
        if (crypt_hash_init(&md, hash_name))
                return -ENOENT;
        len = crypt_hash_size(hash_name);
        for(round = 0; key_size && !r; round++) {
                /* hack from hashalot to avoid null bytes in key */
                for(i = 0; i < round; i++)
                        if (crypt_hash_write(md, "A", 1))
                                r = 1;
                if (crypt_hash_write(md, passphrase, passphrase_size))
                        r = 1;
                if (len > key_size)
                        len = key_size;
                if (crypt_hash_final(md, key, len))
                        r = 1;
                key += len;
                key_size -= len;
        }
        crypt_hash_destroy(md);
        return r;
}

Several wrapper function are used above (crypt_hash_*) whose implementation can be found in several places and depend on particular setup. Mine uses openssl and uses lib/crypto_backend/crypto_openssl.c as its implementation.

Uff, that was a lot of jumping around. But now provided for us is some clue in the comment. A hack from hashalot is used to avoid null bytes in key. Great! But what kind of a hack? It looks like if the size len of a hash is smaller than the required size of the key, then after initial password hash is generated, a new hash is calculated from a passphrase with letter "A" appended at the beginnig. This second hash is then gluead at the end of the first one. This is done as many times as needed to end up with required key length.

I managed to write a small program that recreates this approach:

#include <stdio.h>
#include <openssl/evp.h>
#include <string.h>
const EVP_MD *md;
EVP_MD_CTX *md_ctx;
void print_key(unsigned char key[], int key_size){
  printf("key is: ");
  for (int i = 0; i < key_size; i++){
    printf("%02x",key[i]);
  }
  printf("\n");
}
int main(){
  char *hash_name = "ripemd160";
  int hash_len = 0;
  char *passphrase = "password";
  int pass_size = strlen(passphrase);
  int key_size = 32;
  unsigned char key[key_size];
  unsigned char key_copy[key_size];
  int key_index = 0;
  const int key_size_c = key_size;
  md_ctx = EVP_MD_CTX_new();
  if (!md_ctx){
    printf("Error createing MD_CTX\n");
    return 1;
  }
  md = EVP_get_digestbyname(hash_name);
  if (!md){
    printf("Error getting digest by name\n");
    return 1;
  }
  if (EVP_DigestInit_ex(md_ctx, md, NULL) != 1){
    printf("Error initializing md\n");
    return 1;
  }
  hash_len = EVP_MD_size(md);
  unsigned int *tmp_len = 0;
  int len = hash_len;
  for (int round = 0; key_size; round++){
    for (int i=0; i < round; i++){
      if (EVP_DigestUpdate(md_ctx, "A", 1) != 1){
        printf("Inside digest update failed\n");
        return 1;
      }
    }
      if (EVP_DigestUpdate(md_ctx, passphrase, pass_size) != 1){
        printf("Outside digest update failed\n");
        return 1;
      }
      if (len > key_size){
        len = key_size;
      }
      if (EVP_DigestFinal_ex(md_ctx, key, tmp_len) != 1){
        printf("Digest final failed\n");
        return 1;
      }
      memcpy(&key_copy[key_index],key,len);
      key_index += len;
      key_size -= len;
      if (EVP_DigestInit_ex(md_ctx, md, NULL) != 1){
        printf("Digest restart failed\n");
        return 1;
      }
  }
  print_key(key_copy,key_size_c);
  return(0);
}

Let's compile it and check the result:

$ gcc main.c -lcryptsetup -lcrypto -g
$ ./a.out
key is: 2c08e8f5884750a7b99f6f2f342fc638db25ff31fb00ff3460badc65d7758851

Success! Now in case of emergency I will be able to mount plain-encrypted volumes with dmsetup without resorting to cryptsetup :)