Or how to make dm-crypt work backwards!

This is probably not practically useful but it's a neat proof of concept solution to a real-life problem! We run a Ceph cluster for the majority of our data and we're looking into using a managed backup-as-a-service thingy, which is fine, but we have requirements that the backup service isn't allowed to see or store any of our plaintext data. Totally reasonable but a little hard to swing on Ceph — although the backing disks are encrypted the actual disk images hosted there aren't.

A normal solution would probably look like grabbing chunks of each image on a separate server, encrypting them, and then giving those to the backup agent to store. Very much like what OVH does. But the reason OVH went this route was because of limitations of Duplicity, and the backup agent we're using handles huge block devices just fine. So we're going through this just to encrypt some data in-flight and then we have to do some awkward reassembly dance when we restore. If only we were trying to decrypt the data it would be no issue. We would just use dm-crypt and the kernel would decrypt on the fly for us and the backup agent would be none the wiser.

Can we run this process backwards?

Shout out to Arno Wagner for this thread posted in 2013. Assuming your problem is still unimplemented 7 years later I got you sorted! After a lot of back-and-forth we finally get to a potential way of accomplishing this.

I think that quick hack to try it would be to write simple kernel cipher module (or wrapper), where you only change cipher name (so it will not mix up with normal implementation, name like reverse_aes or so) and just switch encrypt/decrypt callbacks.

I am afraid you will need to avoid LUKS and IV where encryption is used (ESSIV) (or at least you must analyze if encrypt/decrypt change for the given cipher is safe for use there).

I'm going to describe how to implement what the poster suggests. At the end I will have a block cipher revaes and I will use it like cryptsetup plainOpen --cipher revaes /dev/sdb1 sdb1_enc. Can any Linux kernel or crypto enthusiasts call in advance why this won't work? And if you get that, can you call in advance how to use an existing cipher to do what I want?

Let's start with a bare-bones Makefile to actually build the module.

obj-m += revaes.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Wonderful! So all we need to do is write a file revaes.c with the module and we're done. Let's poke around the kernel source and see if there's anything we can use as a guide. Looks like [aes_ti.c](<https://github.com/torvalds/linux/blob/b25c6644bfd3affd7d0127ce95c5c96c155a7515/crypto/aes_ti.c>) is short, sweet, and has all the bits I need. Let's just touch it up a bit.

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Reversed AES core transform
 *
 * Copyright (C) 2017 Linaro Ltd <[email protected]>
 */

#include <crypto/aes.h>
#include <linux/crypto.h>
#include <linux/module.h>

static int revaes_set_key(struct crypto_tfm *tfm, const u8 *in_key,
			 unsigned int key_len)
{
	struct crypto_aes_ctx *ctx = crypto_tfm_ctx(tfm);

	return aes_expandkey(ctx, in_key, key_len);
}

static void revaes_encrypt(struct crypto_tfm *tfm, u8 *out, const u8 *in)
{
	const struct crypto_aes_ctx *ctx = crypto_tfm_ctx(tfm);

	aes_encrypt(ctx, out, in);
}

static void revaes_decrypt(struct crypto_tfm *tfm, u8 *out, const u8 *in)
{
	const struct crypto_aes_ctx *ctx = crypto_tfm_ctx(tfm);

	aes_decrypt(ctx, out, in);
}

static struct crypto_alg aes_alg = {
	.cra_name			    = "revaes",
	.cra_driver_name	= "revaes-generic",
	.cra_priority			= 100,
	.cra_flags			  = CRYPTO_ALG_TYPE_CIPHER,
	.cra_blocksize		= AES_BLOCK_SIZE,
	.cra_ctxsize			= sizeof(struct crypto_aes_ctx),
	.cra_module			  = THIS_MODULE,
	.cra_cipher.cia_min_keysize	= AES_MIN_KEY_SIZE,
	.cra_cipher.cia_max_keysize	= AES_MAX_KEY_SIZE,
	.cra_cipher.cia_setkey		= revaes_set_key,
	.cra_cipher.cia_encrypt		= revaes_decrypt,
	.cra_cipher.cia_decrypt		= revaes_encrypt
};

static int __init aes_init(void)
{
	return crypto_register_alg(&aes_alg);
}

static void __exit aes_fini(void)
{
	crypto_unregister_alg(&aes_alg);
}

module_init(aes_init);
module_exit(aes_fini);

MODULE_DESCRIPTION("Generic reversed AES");
MODULE_AUTHOR("Ard Biesheuvel <[email protected]>");
MODULE_LICENSE("GPL v2");

Sweet! Let's build it, load it, and try using it!

make
sudo insmod revaes.ko
sudo cryptsetup plainOpen --cipher revaes /dev/sdb1 sdb1_enc
sudo cryptsetup plainOpen --cipher aes /dev/mapper/sdb1_enc sdb1_plain
mount /dev/mapper/sdb1_pain /mnt/tmp
# Err. Bad superblock.

Welp. What went wrong?

cryptsetup status sdb1_enc
/dev/mapper/sdb1_env is active.
  type:    PLAIN
  cipher:  aes-cbc-plain
  keysize: 256 bits
  key location: dm-crypt
  device:  /dev/sdb1
  sector size:  512
  offset:  0 sectors
  size:    12345 sectors
  mode:    read/write

This is the point where I banged my head for a while and where the crypto nerds should be frustratedly shouting at the screen like when Dora should clearly be able to see that the largo plank fits in the hole in the bridge.

Let's talk about cipher modes or chainmodes as the kernel calls them. If you look in the kernel docs you'll see the very underdescribed dm-crypt cipher specification.

cipher[:keycount]-chainmode-ivmode[:ivopts]
or
capi:cipher_api_spec-ivmode[:ivopts]

Zero idea what these are. And outside of the examples it's not even clear what values are permissible in each of the slots. Is is clear I probably shouldn't even be touching crypto code? But where's the fun in that?! How else does one learn? Luckily Wikipedia has my back with a fantastic article on the subject.