/*
 * Copyright (c) 2013, The Linux Foundation. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 *       copyright notice, this list of conditions and the following
 *       disclaimer in the documentation and/or other materials provided
 *       with the distribution.
 *     * Neither the name of The Linux Foundation nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <inttypes.h>
#include <linux/qseecom.h>
#include <linux/msm_ion.h>

/* Service IDs */
#define SCM_SVC_SSD                 0x07

/* Service specific command IDs */
#define SSD_PARSE_MD_ID             0x06
#define SSD_DECRYPT_IMG_FRAG_ID     0x07

/* SSD parsing status messages from TZ */
#define SSD_PMD_ENCRYPTED           0
#define SSD_PMD_NOT_ENCRYPTED       1
#define SSD_PMD_PARSING_INCOMPLETE  6

#define SSD_HEADER_MIN_SIZE         128
#define MULTIPLICATION_FACTOR       2

#define SMCMOD_DECRYPT_REQ_OP_METADATA  1
#define SMCMOD_DECRYPT_REQ_OP_IMG_FRAG  2

struct smcmod_decrypt_req {
    uint32_t service_id; /* in */
    uint32_t command_id; /* in */
    uint32_t operation;  /* in */

    union {
        struct {
            uint32_t len;
            uint32_t ion_fd;
        } metadata;
        struct {
            uint32_t ctx_id;
            uint32_t last_frag;
            uint32_t frag_len;
            uint32_t ion_fd;
            uint32_t offset;
        } img_frag;
    } request;

    union {
        struct {
            uint32_t status;
            uint32_t ctx_id;
            uint32_t end_offset;
        } metadata;
        struct {
            uint32_t status;
        } img_frag;
    } response;
};

#define SMCMOD_IOC_MAGIC    0x97
#define SMCMOD_IOCTL_DECRYPT_CMD \
    _IOWR(SMCMOD_IOC_MAGIC, 37, struct smcmod_decrypt_req)

struct ion_buf_handle {
    unsigned char *buffer;
    uint32_t buffer_len;
    int ion_fd;
    int ifd_data_fd;
    struct ion_handle_data ion_alloc_handle;
};

static int
ion_memalloc(struct ion_buf_handle *buf, uint32_t size, uint32_t heap)
{
    struct ion_allocation_data alloc_data;
    struct ion_fd_data fd_data;
    unsigned char *va;
    struct ion_handle_data handle_data;
    int ion_fd;
    int rc;

    ion_fd = open("/dev/ion", O_RDONLY);
    if (ion_fd < 0) {
        fprintf(stderr, "Cannot open ION device (%s)\n", strerror(errno));
        return -1;
    }

    alloc_data.len = (size + 4095) & ~4095;
    alloc_data.align = 4096;

    alloc_data.flags = 0;
    alloc_data.heap_id_mask = ION_HEAP(heap);

    /* Set the buffers to be uncached */
    alloc_data.flags = 0;

    rc = ioctl(ion_fd, ION_IOC_ALLOC, &alloc_data);
    if (rc) {
        fprintf(stderr, "ION buffer allocation failed (%s)\n",
                strerror(errno));
        goto alloc_fail;
    }

    if (alloc_data.handle) {
        fd_data.handle = alloc_data.handle;
    } else {
        fprintf(stderr, "ION alloc data returned NULL\n");
        rc = -1;
        goto alloc_fail;
    }

    rc = ioctl(ion_fd, ION_IOC_MAP, &fd_data);
    if (rc) {
        fprintf(stderr, "ION map call failed(%s)\n", strerror(errno));
        goto ioctl_fail;
    }

    va = mmap(NULL, alloc_data.len, PROT_READ | PROT_WRITE,
              MAP_SHARED, fd_data.fd, 0);
    if (va == MAP_FAILED) {
        fprintf(stderr, "ION memory map failed (%s)\n", strerror(errno));
        rc = -1;
        goto map_fail;
    }

    buf->ion_fd = ion_fd;
    buf->ifd_data_fd = fd_data.fd;
    buf->buffer = va;
    buf->ion_alloc_handle.handle = alloc_data.handle;
    buf->buffer_len = alloc_data.len;

    memset(buf->buffer, 0, buf->buffer_len);
    return 0;

map_fail:
ioctl_fail:
    handle_data.handle = alloc_data.handle;
    if (buf->ifd_data_fd)
        close(buf->ifd_data_fd);
    rc = ioctl(ion_fd, ION_IOC_FREE, &handle_data);
    if (rc)
        fprintf(stderr, "ION free failed (%s)\n", strerror(errno));
alloc_fail:
    if (ion_fd >= 0)
        close(ion_fd);
    buf->ion_fd = -1;
    return rc;
}

static int ion_memfree(struct ion_buf_handle *handle)
{
    struct ion_handle_data handle_data;
    int ret;

    ret = munmap(handle->buffer, (handle->buffer_len + 4095) & ~4095);
    if (ret)
        fprintf(stderr, "munmap failed (%s)\n", strerror(errno));

    handle_data.handle = handle->ion_alloc_handle.handle;
    close(handle->ifd_data_fd);
    ret = ioctl(handle->ion_fd, ION_IOC_FREE, &handle_data);
    if (ret)
        fprintf(stderr, "ION free failed (%s)\n", strerror(errno));
    close(handle->ion_fd);

    return ret;
}

static int ion_buffer_clean_inval(struct ion_buf_handle *buf, uint32_t cmd)
{
    struct ion_flush_data data;
    int rc;

    data.fd = buf->ifd_data_fd;
    data.vaddr = buf->buffer;
    data.length = buf->buffer_len;
    data.offset = 0;
    data.handle = buf->ion_alloc_handle.handle;

    rc = ioctl(buf->ion_fd, cmd, &data);
    if (rc < 0)
        fprintf(stderr, "clean_inval cache failed (%s)\n", strerror(errno));

    return rc;
}

static int is_encrypted(int smcmod_fd, struct ion_buf_handle *buf,
                        uint32_t len, uint32_t *ctx_id, uint32_t *end_offset)
{
    struct smcmod_decrypt_req req;
    uint32_t status;
    int ret;

    req.service_id = SCM_SVC_SSD;
    req.command_id = SSD_PARSE_MD_ID;
    req.operation = SMCMOD_DECRYPT_REQ_OP_METADATA;
    req.request.metadata.len =
        (len >= SSD_HEADER_MIN_SIZE) ? SSD_HEADER_MIN_SIZE : len;
    req.request.metadata.ion_fd = buf->ifd_data_fd;

    do {
        ret = ioctl(smcmod_fd, SMCMOD_IOCTL_DECRYPT_CMD, &req);
        if (ret < 0)
            fprintf(stderr, "%s: ioctl ret=%d, %s\n", __func__, ret,
                    strerror(errno));

        status = req.response.metadata.status;

        if (!ret && (status == SSD_PMD_PARSING_INCOMPLETE)) {
            req.request.metadata.len *= MULTIPLICATION_FACTOR;
            continue;
        } else {
            break;
        }
    } while (1);

    if (!ret) {
        if (status == SSD_PMD_ENCRYPTED) {
            *ctx_id = req.response.metadata.ctx_id;
            *end_offset = req.response.metadata.end_offset;
            ret = 1;
        } else {
            fprintf(stderr, "Image is not encrypted (response status %d)\n",
                    status);
        }
    } else {
        fprintf(stderr, "%s: call failed\n", __func__);
    }

    return ret;
}

static int decrypt(int smcmod_fd, struct ion_buf_handle *buf, uint32_t len,
                   uint32_t md_ctx, uint32_t offset)
{
    struct smcmod_decrypt_req req;
    int ret;

    req.service_id = SCM_SVC_SSD;
    req.command_id = SSD_DECRYPT_IMG_FRAG_ID;
    req.operation = SMCMOD_DECRYPT_REQ_OP_IMG_FRAG;
    req.request.img_frag.ctx_id = md_ctx;
    req.request.img_frag.last_frag = 1;
    req.request.img_frag.ion_fd = buf->ifd_data_fd;
    req.request.img_frag.frag_len = len - offset;
    req.request.img_frag.offset = offset;

    ret = ioctl(smcmod_fd, SMCMOD_IOCTL_DECRYPT_CMD, &req);
    if (ret < 0) {
        fprintf(stderr, "decrypt ioctl failed (%s)\n", strerror(errno));
        return ret;
    }

    return 0;
}

static int save_file(const char *fname, unsigned char *buf, size_t len)
{
    FILE *file;
    size_t written;

    file = fopen(fname, "wb");
    if (!file) {
        fprintf(stderr, "Failed to open %s (%s)\n", fname, strerror(errno));
        return -errno;
    }

    written = fwrite(buf, len, 1, file);
    if (written != 1) {
        fclose(file);
        fprintf(stderr, "Failed to write %s (%s)\n", fname, strerror(errno));
        return -errno;
    }
    fflush(file);
    fclose(file);
    fprintf(stdout, "%s written %d bytes\n", fname, len);
    return 0;
}

int decrypt_image(const char *src_file, const char *dst_file)
{
    int ret = -1;
    uint32_t md_ctx = 0, offset = 0;
    uint32_t fsize = 0;
    FILE *file = NULL;
    struct ion_buf_handle ionbuf;
    int smcmod_fd = -1;
    int qseecom_fd = -1;
    size_t read;

    memset(&ionbuf, 0, sizeof(ionbuf));
    ionbuf.ion_fd = -1;

    qseecom_fd = open("/dev/qseecom", O_RDWR);
    if (qseecom_fd < 0) {
        fprintf(stderr, "Failed to open /dev/qseecom device (%s)\n",
                strerror(errno));
        goto exit;
    }

    smcmod_fd = open("/dev/smcmod", O_RDWR);
    if (smcmod_fd < 0) {
        fprintf(stderr, "Failed to open /dev/smcmod device (%s)\n",
                strerror(errno));
        goto exit;
    }

    file = fopen(src_file, "rb");
    if (!file) {
        fprintf(stderr, "Failed to open %s (%s)\n", src_file, strerror(errno));
        goto exit;
    }

    fseek(file, 0, SEEK_END);
    fsize = ftell(file);
    fseek(file, 0, SEEK_SET);

    ret = ion_memalloc(&ionbuf, fsize, ION_PIL1_HEAP_ID);
    if (ret)
        goto exit;

    read = fread(ionbuf.buffer, fsize, 1, file);
    if (read != 1) {
        fprintf(stderr, "Failed to read %s (%s)\n", src_file, strerror(errno));
        ret = -errno;
        goto exit;
    }

    ret = ion_buffer_clean_inval(&ionbuf, ION_IOC_CLEAN_INV_CACHES);
    if (ret < 0)
        goto exit;

    ret = ioctl(qseecom_fd, QSEECOM_IOCTL_PERF_ENABLE_REQ);
    if (ret < 0)
        goto exit;

    ret = is_encrypted(smcmod_fd, &ionbuf, fsize, &md_ctx, &offset);
    if (ret < 0)
        goto exit;

    if (ret == 1) {
        fprintf(stdout, "decrypting %s ...\n", src_file);
        ret = decrypt(smcmod_fd, &ionbuf, fsize, md_ctx, offset);
        if (ret < 0)
            goto exit;

        ion_buffer_clean_inval(&ionbuf, ION_IOC_INV_CACHES);

        ret = save_file(dst_file, ionbuf.buffer + offset, fsize - offset);
        if (ret < 0)
            goto exit;

        fprintf(stdout, "decrypting done!\n");
    }

exit:
    if (ionbuf.ion_fd >= 0)
        ion_memfree(&ionbuf);

    if (qseecom_fd >= 0) {
        ioctl(qseecom_fd, QSEECOM_IOCTL_PERF_DISABLE_REQ);
        close(qseecom_fd);
    }

    if (smcmod_fd >= 0)
        close(smcmod_fd);

    if (file)
        fclose(file);

    return ret;
}
