root/drivers/md/dm-inlinecrypt.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright 2024 Google LLC
 */

#include <linux/blk-crypto.h>
#include <linux/ctype.h>
#include <linux/device-mapper.h>
#include <linux/hex.h>
#include <linux/module.h>
#include <keys/user-type.h>

#define DM_MSG_PREFIX   "inlinecrypt"

static const struct dm_inlinecrypt_cipher {
        const char *name;
        enum blk_crypto_mode_num mode_num;
} dm_inlinecrypt_ciphers[] = {
        {
                .name = "aes-xts-plain64",
                .mode_num = BLK_ENCRYPTION_MODE_AES_256_XTS,
        },
};

/**
 * struct inlinecrypt_ctx - private data of an inlinecrypt target
 * @dev: the underlying device
 * @start: starting sector of the range of @dev which this target actually maps.
 *         For this purpose a "sector" is 512 bytes.
 * @cipher_string: the name of the encryption algorithm being used
 * @key_size: size of the encryption key in bytes
 * @iv_offset: starting offset for IVs.  IVs are generated as if the target were
 *             preceded by @iv_offset 512-byte sectors.
 * @sector_size: crypto sector size in bytes (usually 4096)
 * @sector_bits: log2(sector_size)
 * @key_type: type of the key -- either raw or hardware-wrapped
 * @key: the encryption key to use
 * @max_dun: the maximum DUN that may be used (computed from other params)
 */
struct inlinecrypt_ctx {
        struct dm_dev *dev;
        sector_t start;
        const char *cipher_string;
        unsigned int key_size;
        u64 iv_offset;
        unsigned int sector_size;
        unsigned int sector_bits;
        enum blk_crypto_key_type key_type;
        struct blk_crypto_key key;
        u64 max_dun;
};

static const struct dm_inlinecrypt_cipher *
lookup_cipher(const char *cipher_string)
{
        int i;

        for (i = 0; i < ARRAY_SIZE(dm_inlinecrypt_ciphers); i++) {
                if (strcmp(cipher_string, dm_inlinecrypt_ciphers[i].name) == 0)
                        return &dm_inlinecrypt_ciphers[i];
        }
        return NULL;
}

static void inlinecrypt_dtr(struct dm_target *ti)
{
        struct inlinecrypt_ctx *ctx = ti->private;

        if (ctx->dev) {
                if (ctx->key.size)
                        blk_crypto_evict_key(ctx->dev->bdev, &ctx->key);
                dm_put_device(ti, ctx->dev);
        }
        kfree_sensitive(ctx->cipher_string);
        kfree_sensitive(ctx);
}

#ifdef CONFIG_KEYS

static bool contains_whitespace(const char *str)
{
        while (*str)
                if (isspace(*str++))
                        return true;
        return false;
}

static int set_key_user(struct key *key, char *key_bytes,
                        const unsigned int key_bytes_size)
{
        const struct user_key_payload *ukp;

        ukp = user_key_payload_locked(key);
        if (!ukp)
                return -EKEYREVOKED;

        if (key_bytes_size != ukp->datalen)
                return -EINVAL;

        memcpy(key_bytes, ukp->data, key_bytes_size);

        return 0;
}

static int inlinecrypt_get_keyring_key(const char *key_string, u8 *key_bytes,
                                        const unsigned int key_bytes_size)
{
        char *key_desc;
        int ret;
        struct key_type *type;
        struct key *key;
        int (*set_key)(struct key *key, char *key_bytes,
                                   const unsigned int key_bytes_size);

        /*
         * Reject key_string with whitespace. dm core currently lacks code for
         * proper whitespace escaping in arguments on DM_TABLE_STATUS path.
         */
        if (contains_whitespace(key_string)) {
                DMERR("whitespace chars not allowed in key string");
                return -EINVAL;
        }

        /* look for next ':' separating key_type from key_description */
        key_desc = strchr(key_string, ':');
        if (!key_desc || key_desc == key_string || !strlen(key_desc + 1))
                return -EINVAL;

        if (!strncmp(key_string, "logon:", key_desc - key_string + 1)) {
                type = &key_type_logon;
                set_key = set_key_user;
        } else {
                return -EINVAL;
        }

        key = request_key(type, key_desc + 1, NULL);
        if (IS_ERR(key))
                return PTR_ERR(key);

        down_read(&key->sem);

        ret = set_key(key, (char *)key_bytes, key_bytes_size);

        up_read(&key->sem);
        key_put(key);

        return ret;
}

static int get_key_size(char **key_string)
{
        char *colon, dummy;
        int ret;

        if (*key_string[0] != ':') {
                ret = strlen(*key_string);

                if (ret > 2 * BLK_CRYPTO_MAX_ANY_KEY_SIZE
                        || ret  % 2
                        || !ret) {
                        DMERR("Invalid keysize");
                        return -EINVAL;
                }
                return ret >> 1;
        }

        /* look for next ':' in key string */
        colon = strpbrk(*key_string + 1, ":");
        if (!colon)
                return -EINVAL;

        if (sscanf(*key_string + 1, "%u%c", &ret, &dummy) != 2 || dummy != ':')
                return -EINVAL;

        /* remaining key string should be :<logon|user>:<key_desc> */
        *key_string = colon;

        return ret;
}

#else

static int inlinecrypt_get_keyring_key(const char *key_string, u8 *key_bytes,
                                        const unsigned int key_bytes_size)
{
        return -EINVAL;
}

static int get_key_size(char **key_string)
{
        int key_hex_size = strlen(*key_string);

        if (*key_string[0] == ':')
                return -EINVAL;

        if (key_hex_size > 2 * BLK_CRYPTO_MAX_ANY_KEY_SIZE
                || key_hex_size  % 2
                || !key_hex_size) {
                DMERR("Invalid keysize");
                return -EINVAL;
        }

        return key_hex_size >> 1;
}

#endif /* CONFIG_KEYS */

static int inlinecrypt_get_key(const char *key_string,
                                u8 key[BLK_CRYPTO_MAX_ANY_KEY_SIZE],
                                const unsigned int key_size)
{
        int ret = 0;

        if (key_size > BLK_CRYPTO_MAX_ANY_KEY_SIZE) {
                DMERR("Invalid keysize");
                return -EINVAL;
        }

        /* ':' means the key is in kernel keyring, short-circuit normal key processing */
        if (key_string[0] == ':') {
                /* key string should be :<logon|user>:<key_desc> */
                ret = inlinecrypt_get_keyring_key(key_string + 1, key, key_size);
                goto out;
        }

        if (hex2bin(key, key_string, key_size) != 0)
                ret = -EINVAL;

out:
        return ret;
}

static int inlinecrypt_ctr_optional(struct dm_target *ti,
                                    unsigned int argc, char **argv)
{
        struct inlinecrypt_ctx *ctx = ti->private;
        struct dm_arg_set as;
        static const struct dm_arg _args[] = {
                {0, 4, "Invalid number of feature args"},
        };
        unsigned int opt_params;
        const char *opt_string;
        bool iv_large_sectors = false;
        char dummy;
        int err;

        as.argc = argc;
        as.argv = argv;

        err = dm_read_arg_group(_args, &as, &opt_params, &ti->error);
        if (err)
                return err;

        while (opt_params--) {
                opt_string = dm_shift_arg(&as);
                if (!opt_string) {
                        ti->error = "Not enough feature arguments";
                        return -EINVAL;
                }
                if (str_has_prefix(opt_string, "keytype:")) {
                        const char *val = opt_string + strlen("keytype:");

                        if (!*val) {
                                ti->error = "Invalid block key type";
                                return -EINVAL;
                        }

                        if (!strcmp(val, "raw")) {
                                ctx->key_type = BLK_CRYPTO_KEY_TYPE_RAW;
                        } else if (!strcmp(val, "hw-wrapped")) {
                                ctx->key_type = BLK_CRYPTO_KEY_TYPE_HW_WRAPPED;
                        } else {
                                ti->error = "Invalid block key type";
                                return -EINVAL;
                        }
                } else if (!strcmp(opt_string, "allow_discards")) {
                        ti->num_discard_bios = 1;
                } else if (sscanf(opt_string, "sector_size:%u%c",
                                  &ctx->sector_size, &dummy) == 1) {
                        if (ctx->sector_size < SECTOR_SIZE ||
                            ctx->sector_size > 4096 ||
                            !is_power_of_2(ctx->sector_size)) {
                                ti->error = "Invalid sector_size";
                                return -EINVAL;
                        }
                } else if (!strcmp(opt_string, "iv_large_sectors")) {
                        iv_large_sectors = true;
                } else {
                        ti->error = "Invalid feature arguments";
                        return -EINVAL;
                }
        }

        /* dm-inlinecrypt doesn't implement iv_large_sectors=false. */
        if (ctx->sector_size != SECTOR_SIZE && !iv_large_sectors) {
                ti->error = "iv_large_sectors must be specified";
                return -EINVAL;
        }

        return 0;
}

/*
 * Construct an inlinecrypt mapping:
 * <cipher> [<key>|:<key_size>:<logon>:<key_description>] <iv_offset> <dev_path> <start>
 *
 * This syntax matches dm-crypt's, but the set of supported functionality has
 * been stripped down.
 */
static int inlinecrypt_ctr(struct dm_target *ti, unsigned int argc, char **argv)
{
        struct inlinecrypt_ctx *ctx;
        const struct dm_inlinecrypt_cipher *cipher;
        u8 key_bytes[BLK_CRYPTO_MAX_ANY_KEY_SIZE];
        unsigned int dun_bytes;
        unsigned long long tmpll;
        char dummy;
        int err;

        if (argc < 5) {
                ti->error = "Not enough arguments";
                return -EINVAL;
        }

        ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
        if (!ctx) {
                ti->error = "Out of memory";
                return -ENOMEM;
        }
        ti->private = ctx;

        /* <cipher> */
        ctx->cipher_string = kstrdup(argv[0], GFP_KERNEL);
        if (!ctx->cipher_string) {
                ti->error = "Out of memory";
                err = -ENOMEM;
                goto bad;
        }
        cipher = lookup_cipher(ctx->cipher_string);
        if (!cipher) {
                ti->error = "Unsupported cipher";
                err = -EINVAL;
                goto bad;
        }

        /* <key> */
        err = get_key_size(&argv[1]);
        if (err < 0) {
                ti->error = "Cannot parse key size";
                err = -EINVAL;
                goto bad;
        }
        ctx->key_size = err;

        err = inlinecrypt_get_key(argv[1], key_bytes, ctx->key_size);
        if (err) {
                ti->error = "Malformed key string";
                goto bad;
        }

        /* <iv_offset> */
        if (sscanf(argv[2], "%llu%c", &ctx->iv_offset, &dummy) != 1) {
                ti->error = "Invalid iv_offset sector";
                err = -EINVAL;
                goto bad;
        }

        /* <dev_path> */
        err = dm_get_device(ti, argv[3], dm_table_get_mode(ti->table),
                            &ctx->dev);
        if (err) {
                ti->error = "Device lookup failed";
                goto bad;
        }

        /* <start> */
        if (sscanf(argv[4], "%llu%c", &tmpll, &dummy) != 1 ||
            tmpll != (sector_t)tmpll) {
                ti->error = "Invalid start sector";
                err = -EINVAL;
                goto bad;
        }
        ctx->start = tmpll;

        /* optional arguments */
        ctx->sector_size = SECTOR_SIZE;
        ctx->key_type = BLK_CRYPTO_KEY_TYPE_RAW;
        if (argc > 5) {
                err = inlinecrypt_ctr_optional(ti, argc - 5, &argv[5]);
                if (err)
                        goto bad;
        }
        ctx->sector_bits = ilog2(ctx->sector_size);
        if (ti->len & ((ctx->sector_size >> SECTOR_SHIFT) - 1)) {
                ti->error = "Device size is not a multiple of sector_size";
                err = -EINVAL;
                goto bad;
        }
        if (ctx->iv_offset & ((ctx->sector_size >> SECTOR_SHIFT) - 1)) {
                ti->error = "Wrong alignment of iv_offset sector";
                err = -EINVAL;
                goto bad;
        }

        ctx->max_dun = (ctx->iv_offset + ti->len - 1) >>
                       (ctx->sector_bits - SECTOR_SHIFT);
        dun_bytes = DIV_ROUND_UP(fls64(ctx->max_dun), 8);

        err = blk_crypto_init_key(&ctx->key, key_bytes, ctx->key_size,
                                  ctx->key_type, cipher->mode_num,
                                  dun_bytes, ctx->sector_size);
        if (err) {
                ti->error = "Error initializing blk-crypto key";
                goto bad;
        }

        err = blk_crypto_start_using_key(ctx->dev->bdev, &ctx->key);
        if (err) {
                ti->error = "Error starting to use blk-crypto";
                goto bad;
        }

        ti->num_flush_bios = 1;

        err = 0;
        goto out;

bad:
        inlinecrypt_dtr(ti);
out:
        memzero_explicit(key_bytes, sizeof(key_bytes));
        return err;
}

static int inlinecrypt_map(struct dm_target *ti, struct bio *bio)
{
        const struct inlinecrypt_ctx *ctx = ti->private;
        sector_t sector_in_target;
        u64 dun[BLK_CRYPTO_DUN_ARRAY_SIZE] = {};

        bio_set_dev(bio, ctx->dev->bdev);

        /*
         * If the bio is a device-level request which doesn't target a specific
         * sector, there's nothing more to do.
         */
        if (bio_sectors(bio) == 0)
                return DM_MAPIO_REMAPPED;

        /*
         * The bio should never have an encryption context already, since
         * dm-inlinecrypt doesn't pass through any inline encryption
         * capabilities to the layer above it.
         */
        if (WARN_ON_ONCE(bio_has_crypt_ctx(bio)))
                return DM_MAPIO_KILL;

        /* Map the bio's sector to the underlying device. (512-byte sectors) */
        sector_in_target = dm_target_offset(ti, bio->bi_iter.bi_sector);
        bio->bi_iter.bi_sector = ctx->start + sector_in_target;
        /*
         * If the bio doesn't have any data (e.g. if it's a DISCARD request),
         * there's nothing more to do.
         */
        if (!bio_has_data(bio))
                return DM_MAPIO_REMAPPED;

        /* Calculate the DUN and enforce data-unit (crypto sector) alignment. */
        dun[0] = ctx->iv_offset + sector_in_target; /* 512-byte sectors */
        if (dun[0] & ((ctx->sector_size >> SECTOR_SHIFT) - 1))
                return DM_MAPIO_KILL;
        dun[0] >>= ctx->sector_bits - SECTOR_SHIFT; /* crypto sectors */

        /*
         * This check isn't necessary as we should have calculated max_dun
         * correctly, but be safe.
         */
        if (WARN_ON_ONCE(dun[0] > ctx->max_dun))
                return DM_MAPIO_KILL;

        bio_crypt_set_ctx(bio, &ctx->key, dun, GFP_NOIO);

        /*
         * Since we've added an encryption context to the bio and
         * blk-crypto-fallback may be needed to process it, it's necessary to
         * use the fallback-aware bio submission code rather than
         * unconditionally returning DM_MAPIO_REMAPPED.
         *
         * To get the correct accounting for a dm target in the case where
         * __blk_crypto_submit_bio() doesn't take ownership of the bio (returns
         * true), call __blk_crypto_submit_bio() directly and return
         * DM_MAPIO_REMAPPED in that case, rather than relying on
         * blk_crypto_submit_bio() which calls submit_bio() in that case.
         *
         * TODO: blk-crypto fallback write slow-path currently double-accounts
         * IO in vmstat, as encrypted bios are submitted via submit_bio().
         * This does not affect data correctness. Consider fixing this if
         * a cleaner accounting model for derived bios is introduced.
         */
        if (__blk_crypto_submit_bio(bio))
                return DM_MAPIO_REMAPPED;
        return DM_MAPIO_SUBMITTED;
}

static void inlinecrypt_status(struct dm_target *ti, status_type_t type,
                               unsigned int status_flags, char *result,
                               unsigned int maxlen)
{
        const struct inlinecrypt_ctx *ctx = ti->private;
        unsigned int sz = 0;
        int num_feature_args = 0;

        switch (type) {
        case STATUSTYPE_INFO:
        case STATUSTYPE_IMA:
                result[0] = '\0';
                break;

        case STATUSTYPE_TABLE:
                /*
                 * Warning: like dm-crypt, dm-inlinecrypt includes the key in
                 * the returned table.  Userspace is responsible for redacting
                 * the key when needed.
                 */
                DMEMIT("%s %*phN %u %llu %s %llu", ctx->cipher_string,
                       ctx->key.size, ctx->key.bytes,
                       ctx->key_type, ctx->iv_offset,
                       ctx->dev->name, ctx->start);
                num_feature_args += !!ti->num_discard_bios;
                if (ctx->sector_size != SECTOR_SIZE)
                        num_feature_args += 2;
                if (num_feature_args != 0) {
                        DMEMIT(" %d", num_feature_args);
                        if (ti->num_discard_bios)
                                DMEMIT(" allow_discards");
                        if (ctx->sector_size != SECTOR_SIZE) {
                                DMEMIT(" sector_size:%u", ctx->sector_size);
                                DMEMIT(" iv_large_sectors");
                        }
                }
                break;
        }
}

static int inlinecrypt_prepare_ioctl(struct dm_target *ti,
                                     struct block_device **bdev, unsigned int cmd,
                                     unsigned long arg, bool *forward)
{
        const struct inlinecrypt_ctx *ctx = ti->private;
        const struct dm_dev *dev = ctx->dev;

        *bdev = dev->bdev;

        /* Only pass ioctls through if the device sizes match exactly. */
        return ctx->start != 0 || ti->len != bdev_nr_sectors(dev->bdev);
}

static int inlinecrypt_iterate_devices(struct dm_target *ti,
                                       iterate_devices_callout_fn fn,
                                       void *data)
{
        const struct inlinecrypt_ctx *ctx = ti->private;

        return fn(ti, ctx->dev, ctx->start, ti->len, data);
}

#ifdef CONFIG_BLK_DEV_ZONED
static int inlinecrypt_report_zones(struct dm_target *ti,
                                    struct dm_report_zones_args *args,
                                    unsigned int nr_zones)
{
        const struct inlinecrypt_ctx *ctx = ti->private;

        return dm_report_zones(ctx->dev->bdev, ctx->start,
                        ctx->start + dm_target_offset(ti, args->next_sector),
                        args, nr_zones);
}
#else
#define inlinecrypt_report_zones NULL
#endif

static void inlinecrypt_io_hints(struct dm_target *ti,
                                 struct queue_limits *limits)
{
        const struct inlinecrypt_ctx *ctx = ti->private;
        const unsigned int sector_size = ctx->sector_size;

        limits->logical_block_size =
                max_t(unsigned int, limits->logical_block_size, sector_size);
        limits->physical_block_size =
                max_t(unsigned int, limits->physical_block_size, sector_size);
        limits->io_min = max_t(unsigned int, limits->io_min, sector_size);
        limits->dma_alignment = limits->logical_block_size - 1;
}

static struct target_type inlinecrypt_target = {
        .name                   = "inlinecrypt",
        .version                = {1, 0, 0},
        /*
         * Do not set DM_TARGET_PASSES_CRYPTO, since dm-inlinecrypt consumes the
         * crypto capability itself.
         */
        .features               = DM_TARGET_ZONED_HM,
        .module                 = THIS_MODULE,
        .ctr                    = inlinecrypt_ctr,
        .dtr                    = inlinecrypt_dtr,
        .map                    = inlinecrypt_map,
        .status                 = inlinecrypt_status,
        .prepare_ioctl          = inlinecrypt_prepare_ioctl,
        .iterate_devices        = inlinecrypt_iterate_devices,
        .report_zones           = inlinecrypt_report_zones,
        .io_hints               = inlinecrypt_io_hints,
};

module_dm(inlinecrypt);

MODULE_AUTHOR("Eric Biggers <ebiggers@google.com>");
MODULE_AUTHOR("Linlin Zhang <linlin.zhang@oss.qualcomm.com>");
MODULE_DESCRIPTION(DM_NAME " target for inline encryption");
MODULE_LICENSE("GPL");