/* vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: */
/*
 * Copyright (c) 2016 Red Hat, Inc.
 * Author: Nathaniel McCallum <npmccallum@redhat.com>
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "crc32c.h"
#include "luksmeta.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define ALIGN(s, up) (((s) + (up ? 4095 : 0)) & ~4095ULL)
#define LUKS_NSLOTS 8
#define LM_VERSION 1

static const uint8_t LM_MAGIC[] = { 'L', 'U', 'K', 'S', 'M', 'E', 'T', 'A' };

typedef struct __attribute__((packed)) {
    luksmeta_uuid_t uuid;
    uint32_t offset;   /* Bytes from the start of the hole */
    uint32_t length;   /* Bytes */
    uint32_t crc32c;
    uint32_t _reserved; /* Reserved */
} lm_slot_t;

typedef struct __attribute__((packed)) {
    uint8_t magic[sizeof(LM_MAGIC)];
    uint32_t version;
    uint32_t crc32c;
    lm_slot_t slots[LUKS_NSLOTS];
} lm_t;

static bool
uuid_is_zero(const luksmeta_uuid_t uuid)
{
    for (size_t i = 0; i < sizeof(luksmeta_uuid_t); i++) {
        if (uuid[i] != 0)
            return false;
    }

    return true;
}

static inline uint32_t
checksum(lm_t lm)
{
    lm.crc32c = 0;
    return crc32c(0, &lm, sizeof(lm_t));
}

static inline bool
overlap(const lm_t *lm, uint32_t start, size_t end, uint32_t hard_limit)
{
    /* Make sure the data fits the available area in the gap. */
    if (end > hard_limit)
        return true;

    for (int i = 0; i < LUKS_NSLOTS; i++) {
        const lm_slot_t *s = &lm->slots[i];
        uint32_t e = s->offset + s->length;

        if (start <= s->offset && s->offset < end)
            return true;

        if (start < e && e <= end)
            return true;
    }

    return false;
}

static inline uint32_t
find_gap(const lm_t *lm, uint32_t length, size_t size)
{
    size = ALIGN(size, true);

    /* Make sure the data is not larger than the total available
     * area in the gap. */
    if (length < size)
        return 0;

    for (uint32_t off = ALIGN(1, true); off < length; off += ALIGN(1, true)) {
        if (!overlap(lm, off, off + size, lm->slots[0].offset + length))
            return off;
    }

    return 0;
}

static int
find_unused_slot(struct crypt_device *cd, const lm_t *lm)
{
    for (int slot = 0; slot < LUKS_NSLOTS; slot++) {
        if (crypt_keyslot_status(cd, slot) == CRYPT_SLOT_INACTIVE &&
            uuid_is_zero(lm->slots[slot].uuid))
            return slot;
    }

    return -1;
}

static inline ssize_t
readall(int fd, void *data, size_t size)
{
    uint8_t *tmp = data;

    for (ssize_t r, t = 0; t < (ssize_t) size; t += r) {
        r = read(fd, &tmp[t], size - t);
        if (r < 0 && errno != EAGAIN)
            return -errno;
        if (r == 0)
            return -ENOENT;
    }

    return size;
}

static inline ssize_t
writeall(int fd, const void *buf, size_t size)
{
    const uint8_t *tmp = buf;

    for (ssize_t r, t = 0; t < (ssize_t) size; t += r) {
        r = write(fd, &tmp[t], size - t);
        if (r < 0) {
            if (errno != EAGAIN)
                return -errno;
            r = 0;
        }
    }

    return size;
}

/**
 * Opens the device with the specified flags.
 *
 * The length parameter is set to the amount of space in the gap between the
 * end of the last slot and the start of the encrypted data.
 *
 * The function returns either the file descriptor positioned to the start of
 * the hole or a negative errno.
 */
static int
open_hole(struct crypt_device *cd, int flags, uint32_t *length)
{
    const char *name = NULL;
    const char *type = NULL;
    uint64_t hole = 0;
    uint64_t data = 0;
    int fd = 0;
    int r = 0;

    type = crypt_get_type(cd);
    if (!type || strcmp(CRYPT_LUKS1, type) != 0)
        return -ENOTSUP;

    data = crypt_get_data_offset(cd) * 512;
    if (data < 4096)
        return -ENOSPC;

    for (int slot = 0; slot < LUKS_NSLOTS; slot++) {
        uint64_t off = 0;
        uint64_t len = 0;

        r = crypt_keyslot_area(cd, slot, &off, &len);
        if (r < 0)
            return r;

        if (hole < off + len)
            hole = ALIGN(off + len, true);
    }

    if (hole == 0)
        return -ENOTSUP;

    if (hole >= data)
        return -ENOSPC;

    name = crypt_get_device_name(cd);
    if (!name)
        return -ENOTSUP;

    fd = open(name, flags);
    if (fd < 0)
        return -errno;

    if (lseek(fd, hole, SEEK_SET) == -1) {
        close(fd);
        return -errno;
    }

    *length = ALIGN(data - hole, false);
    return fd;
}

static int
read_header(struct crypt_device *cd, int flags, uint32_t *length, lm_t *lm)
{
    uint32_t maxlen;
    int fd = -1;
    int r = 0;

    fd = open_hole(cd, flags, length);
    if (fd < 0)
        return fd;

    r = *length >= sizeof(lm_t) ? 0 : -ENOENT;
    if (r < 0)
        goto error;

    r = readall(fd, lm, sizeof(lm_t));
    if (r < 0)
        goto error;

    r = memcmp(LM_MAGIC, lm->magic, sizeof(LM_MAGIC)) == 0 ? 0 : -ENOENT;
    if (r < 0)
        goto error;

    r = lm->version == htobe32(LM_VERSION) ? 0 : -ENOTSUP;
    if (r < 0)
        goto error;

    lm->crc32c = be32toh(lm->crc32c);
    r = checksum(*lm) == lm->crc32c ? 0 : -EINVAL;
    if (r < 0)
        goto error;

    lm->version = be32toh(lm->version);

    maxlen = *length - ALIGN(sizeof(lm_t), true);
    for (int slot = 0; slot < LUKS_NSLOTS; slot++) {
        lm_slot_t *s = &lm->slots[slot];

        s->offset = be32toh(s->offset);
        s->length = be32toh(s->length);
        s->crc32c = be32toh(s->crc32c);

        if (!uuid_is_zero(s->uuid)) {
            r = s->offset > sizeof(lm_t) ? 0 : -EINVAL;
            if (r < 0)
                goto error;

            r = s->length <= maxlen ? 0 : -EINVAL;
            if (r < 0)
                goto error;
        }
    }

    return fd;

error:
    close(fd);
    return r;
}

static int
write_header(int fd, lm_t lm)
{
    for (int slot = 0; slot < LUKS_NSLOTS; slot++) {
        lm.slots[slot].offset = htobe32(lm.slots[slot].offset);
        lm.slots[slot].length = htobe32(lm.slots[slot].length);
        lm.slots[slot].crc32c = htobe32(lm.slots[slot].crc32c);
    }

    memcpy(lm.magic, LM_MAGIC, sizeof(LM_MAGIC));
    lm.version = htobe32(LM_VERSION);
    lm.crc32c = htobe32(checksum(lm));
    return writeall(fd, &lm, sizeof(lm));
}

int
luksmeta_test(struct crypt_device *cd)
{
    int fd = -1;

    fd = read_header(cd, O_RDONLY, &(uint32_t) {0}, &(lm_t) {});
    if (fd >= 0) {
        close(fd);
        return 0;
    }

    return fd;
}

int
luksmeta_nuke(struct crypt_device *cd)
{
    uint8_t zero[ALIGN(1, true)] = {};
    uint32_t length = 0;
    int fd = -1;
    int r = 0;

    fd = open_hole(cd, O_RDWR | O_SYNC, &length);
    if (fd < 0)
        return fd;

    for (size_t i = 0; r >= 0 && i < length; i += sizeof(zero))
        r = writeall(fd, zero, sizeof(zero));

    close(fd);
    return r < 0 ? r : 0;
}

int
luksmeta_init(struct crypt_device *cd)
{
    uint32_t length = 0;
    int fd = -1;
    int r = 0;

    r = luksmeta_test(cd);
    if (r == 0)
        return -EALREADY;
    else if (r != -ENOENT && r != -EINVAL)
        return r;

    fd = open_hole(cd, O_RDWR | O_SYNC, &length);
    if (fd < 0)
        return fd;

    if (length < ALIGN(sizeof(lm_t), true)) {
        close(fd);
        return -ENOSPC;
    }

    r = write_header(fd, (lm_t) {});
    close(fd);
    return r > 0 ? 0 : r;
}

int
luksmeta_load(struct crypt_device *cd, int slot,
              luksmeta_uuid_t uuid, void *buf, size_t size)
{
    uint32_t length = 0;
    lm_slot_t *s = NULL;
    lm_t lm = {};
    int fd = -1;
    int r = 0;

    if (slot < 0 || slot >= LUKS_NSLOTS)
        return -EBADSLT;
    s = &lm.slots[slot];

    fd = read_header(cd, O_RDONLY, &length, &lm);
    if (fd < 0)
        return fd;

    r = uuid_is_zero(s->uuid) ? -ENODATA : 0;
    if (r < 0)
        goto error;

    if (buf) {
        r = size >= s->length ? 0 : -E2BIG;
        if (r < 0)
            goto error;

        r = lseek(fd, s->offset - sizeof(lm), SEEK_CUR) == -1 ? -errno : 0;
        if (r < 0)
            goto error;

        r = readall(fd, buf, s->length);
        if (r < 0)
            goto error;

        r = crc32c(0, buf, s->length) == s->crc32c ? 0 : -EINVAL;
        if (r < 0)
            goto error;
    }

    memcpy(uuid, s->uuid, sizeof(luksmeta_uuid_t));
    close(fd);
    return s->length;

error:
    close(fd);
    return r;
}

int
luksmeta_save(struct crypt_device *cd, int slot,
              const luksmeta_uuid_t uuid, const void *buf, size_t size)
{
    uint32_t length = 0;
    lm_slot_t *s = NULL;
    lm_t lm = {};
    int fd = -1;
    int r = 0;
    off_t off;

    if (uuid_is_zero(uuid))
        return -EKEYREJECTED;

    fd = read_header(cd, O_RDWR | O_SYNC, &length, &lm);
    if (fd < 0)
        return fd;

    if (slot == CRYPT_ANY_SLOT)
        slot = find_unused_slot(cd, &lm);

    r = slot >= 0 && slot < LUKS_NSLOTS ? 0 : -EBADSLT;
    if (r < 0)
        goto error;
    s = &lm.slots[slot];

    r = uuid_is_zero(s->uuid) ? 0 : -EALREADY;
    if (r < 0)
        goto error;

    s->offset = find_gap(&lm, length, size);
    r = s->offset >= ALIGN(sizeof(lm), true) ? 0 : -ENOSPC;
    if (r < 0)
        goto error;

    memcpy(s->uuid, uuid, sizeof(luksmeta_uuid_t));
    s->length = size;
    s->crc32c = crc32c(0, buf, size);

    off = s->offset - sizeof(lm);
    r = lseek(fd, off, SEEK_CUR) == -1 ? -errno : 0;
    if (r < 0)
        goto error;

    r = writeall(fd, buf, size);
    if (r < 0)
        goto error;

    off = s->offset + s->length;
    r = lseek(fd, -off, SEEK_CUR) == -1 ? -errno : 0;
    if (r < 0)
        goto error;

    r = write_header(fd, lm);

error:
    close(fd);
    return r < 0 ? r : slot;
}

int
luksmeta_wipe(struct crypt_device *cd, int slot, const luksmeta_uuid_t uuid)
{
    uint8_t *zero = NULL;
    uint32_t length = 0;
    lm_slot_t *s = NULL;
    lm_t lm = {};
    int fd = -1;
    int r = 0;
    off_t off;

    if (slot < 0 || slot >= LUKS_NSLOTS)
        return -EBADSLT;
    s = &lm.slots[slot];

    fd = read_header(cd, O_RDWR | O_SYNC, &length, &lm);
    if (fd < 0)
        return fd;

    r = uuid_is_zero(s->uuid) ? -EALREADY : 0;
    if (r < 0)
        goto error;

    if (uuid && memcmp(uuid, s->uuid, sizeof(luksmeta_uuid_t)) != 0) {
        r = -EKEYREJECTED;
        goto error;
    }

    off = s->offset - sizeof(lm_t);
    r = lseek(fd, off, SEEK_CUR) == -1 ? -errno : 0;
    if (r < 0)
        goto error;

    r = (zero = calloc(1, s->length)) ? 0 : -errno;
    if (r < 0)
        goto error;

    r = writeall(fd, zero, s->length);
    free(zero);
    if (r < 0)
        goto error;

    off = s->offset + s->length;
    r = lseek(fd, -off, SEEK_CUR) == -1 ? -errno : 0;
    if (r < 0)
        goto error;

    memset(s, 0, sizeof(lm_slot_t));
    r = write_header(fd, lm);

error:
    close(fd);
    return r < 0 ? r : 0;
}
