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:
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:
run_action
then invokes handler
method of action
struct.
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:
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
:
Next stop is process_key
function:
that kicks as to crypt_plain_hash
function:
This function is implemented in lib/crypt_plain.c
and send us to our final destination, which is the function hash
:
hash
is just above crypt_plain_hash
and looks like this:
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:
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 :)