/*  patchmii_core -- low-level functions to handle the downloading, patching
 and installation of updates on the Wii

 Copyright (C) 2008 bushing / hackmii.com
 Copyright (C) 2008 WiiGator
 Copyright (C) 2009 svenpeter
 Copyright (C) 2009 Hermes

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, version 2.

 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 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <gccore.h>
#include <malloc.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include "cert_sys.h"
#include "Error.h"
#include "gecko.h"
#include "IOSPatcher.h"
#include "nus.h"
#include "patchmii_core.h"
#include "sha1.h"
#include "tools.h"

#define TEMP_DIR "/tmp/patchmii"

#define ALIGN(a,b) ((((a)+(b)-1)/(b))*(b))
#define BLOCK 0x1000

int http_status = 0;
int tmd_dirty = 0, tik_dirty = 0, temp_ios_slot = 0;
static u8 encrypt_iv[16];

// yeah, yeah, I know.
signed_blob *s_tmd = NULL, *s_tik = NULL, *s_certs = NULL;
static u8 tmdbuf[MAX_SIGNED_TMD_SIZE] ATTRIBUTE_ALIGN(0x20);
static u8 tikbuf[STD_SIGNED_TIK_SIZE] ATTRIBUTE_ALIGN(0x20);

char ascii(char s) {
    if (s < 0x20) return '.';
    if (s > 0x7E) return '.';
    return s;
}

void hexdump(FILE *fp, void *d, int len) {
    u8 *data;
    int i, off;
    data = (u8*) d;
    for (off = 0; off < len; off += 16) {
        fprintf(fp, "%08x  ", off);
        for (i = 0; i < 16; i++) {
            if ((i + off) >= len)
                fprintf(fp, "   ");
            else
                fprintf(fp, "%02x ", data[off + i]);
        }
        fprintf(fp, " ");
        for (i = 0; i < 16; i++) {
            if ((i + off) >= len)
                fprintf(fp, " ");
            else
                fprintf(fp, "%c", ascii(data[off + i]));
        }
        fprintf(fp, "\n");
    }
}

void printvers(void) {
    gprintf("IOS Version: %08x\n", *((u32*)0xC0003140));
}

void decrypt_buffer(u16 index, u8 *source, u8 *dest, u32 len) {
    static u8 iv[16];
    if (!source) {
        gprintf("decrypt_buffer: invalid source paramater\n");
        ReturnToLoader();
    }
    if (!dest) {
        gprintf("decrypt_buffer: invalid dest paramater\n");
        ReturnToLoader();
    }

    memset(iv, 0, 16);
    memcpy(iv, &index, 2);
    aes_decrypt(iv, source, dest, len);
}

void set_encrypt_iv(u16 index) {
    memset(encrypt_iv, 0, 16);
    memcpy(encrypt_iv, &index, 2);
}

void encrypt_buffer(u8 *source, u8 *dest, u32 len) {
    aes_encrypt(encrypt_iv, source, dest, len);
}

int create_temp_dir(void) {
    int retval;
    // Try to delete the temp directory in case we're starting over
    ISFS_Delete(TEMP_DIR);
    retval = ISFS_CreateDir(TEMP_DIR, 0, 3, 1, 1);
    if (retval) gprintf("ISFS_CreateDir(/tmp/patchmii) returned %d\n", retval);
    return retval;
}

u32 save_nus_object(u16 index, u8 *buf, u32 size) {
    char filename[256];
    static u8 bounce_buf[1024] ATTRIBUTE_ALIGN(0x20);
    u32 i;
    int retval, fd;
    
    snprintf(filename, sizeof(filename), "/tmp/patchmii/%08x", index);
    retval = ISFS_CreateFile(filename, 0, 3, 1, 1);
    if (retval != ISFS_OK) {
        gprintf("ISFS_CreateFile(%s) returned %d\n", filename, retval);
        return retval;
    }

    fd = ISFS_Open(filename, ISFS_ACCESS_WRITE);
    if (fd < 0) {
        gprintf("ISFS_OpenFile(%s) returned %d\n", filename, fd);
        return retval;
    }

    for (i = 0; i < size;) {
        u32 numbytes = ((size - i) < 1024) ? size - i : 1024;
        //spinner();
        memcpy(bounce_buf, buf + i, numbytes);
        retval = ISFS_Write(fd, bounce_buf, numbytes);
        if (retval < 0) {
            gprintf("ISFS_Write(%d, %p, %d) returned %d at offset %d\n",
                    fd, bounce_buf, numbytes, retval, i);
            ISFS_Close(fd);
            return retval;
        }
        i += retval;
    }
    ISFS_Close(fd);
    return size;
}

s32 install_nus_object(tmd *p_tmd, u16 index) {
    char filename[256];
    static u8 bounce_buf1[1024] ATTRIBUTE_ALIGN(0x20);
    static u8 bounce_buf2[1024] ATTRIBUTE_ALIGN(0x20);
    u32 i;
    const tmd_content *p_cr = TMD_CONTENTS(p_tmd);
    //  gprintf("install_nus_object(%p, %lu)\n", p_tmd, index);

    int retval, fd, cfd, ret;
    snprintf(filename, sizeof(filename), "/tmp/patchmii/%08x", p_cr[index].cid);
    
    //spinner();
    fd = ISFS_Open(filename, ISFS_ACCESS_READ);
    if (fd < 0) {
        gprintf("ISFS_OpenFile(%s) returned %d\n", filename, fd);
        return fd;
    }
    set_encrypt_iv(index);
    //  gprintf("ES_AddContentStart(%016llx, %x)\n", p_tmd->title_id, index);

    cfd = ES_AddContentStart(p_tmd->title_id, p_cr[index].cid);
    if (cfd < 0) {
        gcprintf("\nERROR: ES_AddContentStart(%016llx, %x) failed: %s\n",
            p_tmd->title_id, index, EsErrorCodeString(cfd));
        ES_AddTitleCancel();
        return -1;
    }
    // gprintf("\b (cfd %d): ",cfd);
    for (i = 0; i < p_cr[index].size;) {
        u32 numbytes = ((p_cr[index].size - i) < 1024)
            ? p_cr[index].size - i : 1024;
        numbytes = ALIGN(numbytes, 32);
        retval = ISFS_Read(fd, bounce_buf1, numbytes);
        if (retval < 0) {
            gprintf("ISFS_Read(%d, %p, %d) returned %d at offset %d\n", fd, bounce_buf1, numbytes, retval, i);
            ES_AddContentFinish(cfd);
            ES_AddTitleCancel();
            ISFS_Close(fd);
            return retval;
        }
        encrypt_buffer(bounce_buf1, bounce_buf2, sizeof(bounce_buf1));
        ret = ES_AddContentData(cfd, bounce_buf2, retval);
        if (ret < 0) {
            gcprintf("ERROR: ES_AddContentData(%d, %p, %d) returned %s\n", cfd,
                bounce_buf2, retval, EsErrorCodeString(ret));
            ES_AddContentFinish(cfd);
            ES_AddTitleCancel();
            ISFS_Close(fd);
            return ret;
        }
        i += retval;
    }

    // gprintf("\b  done! (0x%x bytes)\n",i);
    ret = ES_AddContentFinish(cfd);
    if (ret < 0) {
        gcprintf("ERROR ES_AddContentFinish failed: %s\n", EsErrorCodeString(
            ret));
        ES_AddTitleCancel();
        ISFS_Close(fd);
        return -1;
    }
    //spinner();
    ISFS_Close(fd);
    return 0;
}

int get_title_key2(signed_blob *s_tik, u8 *key) {
    static u8 iv[16] ATTRIBUTE_ALIGN(0x20);
    static u8 keyin[16] ATTRIBUTE_ALIGN(0x20);
    static u8 keyout[16] ATTRIBUTE_ALIGN(0x20);
    int retval;
    const tik *p_tik;
    
    p_tik = (tik*) SIGNATURE_PAYLOAD(s_tik);
    u8 *enc_key = (u8 *) &p_tik->cipher_title_key;
    memcpy(keyin, enc_key, sizeof keyin);
    memset(keyout, 0, sizeof keyout);
    memset(iv, 0, sizeof iv);
    memcpy(iv, &p_tik->titleid, sizeof p_tik->titleid);
    
    retval = ES_Decrypt(ES_KEY_COMMON, iv, keyin, sizeof keyin, keyout);
    if (retval) gprintf("ES_Decrypt returned %d\n", retval);
    memcpy(key, keyout, sizeof keyout);
    return retval;
}

int change_ticket_title_id(signed_blob *s_tik, u32 titleid1, u32 titleid2) {
    static u8 iv[16] ATTRIBUTE_ALIGN(0x20);
    static u8 keyin[16] ATTRIBUTE_ALIGN(0x20);
    static u8 keyout[16] ATTRIBUTE_ALIGN(0x20);
    int retval;
    tik *p_tik = (tik*) SIGNATURE_PAYLOAD(s_tik);
    u8 *enc_key = (u8 *) &p_tik->cipher_title_key;
    memcpy(keyin, enc_key, sizeof keyin);
    memset(keyout, 0, sizeof keyout);
    memset(iv, 0, sizeof iv);
    memcpy(iv, &p_tik->titleid, sizeof p_tik->titleid);
    
    retval = ES_Decrypt(ES_KEY_COMMON, iv, keyin, sizeof keyin, keyout);
    p_tik->titleid = (u64) titleid1 << 32 | (u64) titleid2;
    memset(iv, 0, sizeof iv);
    memcpy(iv, &p_tik->titleid, sizeof p_tik->titleid);
    
    retval = ES_Encrypt(ES_KEY_COMMON, iv, keyout, sizeof keyout, keyin);
    if (retval) gprintf("ES_Decrypt returned %d\n", retval);
    memcpy(enc_key, keyin, sizeof keyin);
    tik_dirty = 1;
    
    return retval;
}

s32 get_title_version(u32 titleid1, u32 titleid2) {
    u32 tmdsize = 0;
    s32 retval;
    static char tmd_buf[1024] ATTRIBUTE_ALIGN(32);
    signed_blob *stmd = (signed_blob *) &tmd_buf[0];
    int version;
    u64 titleid = (u64) titleid1 << 32 | (u64) titleid2;
    retval = ES_GetStoredTMDSize(titleid, &tmdsize);
    if (retval < 0) {
        if (retval != -106) gprintf("ES_GetStoredTMDSize(%llx) = %x, retval=%d\n", titleid, tmdsize, retval);
        return retval;
    }

    retval = ES_GetStoredTMD(titleid, stmd, tmdsize);
    
    if (retval < 0) {
        gprintf("ES_GetStoredTMD returned %d\n", retval);
        return retval;
    }

    tmd *mytmd = (tmd*) SIGNATURE_PAYLOAD(stmd);
    version = mytmd->title_version;
    return version;
}

void change_tmd_version(signed_blob *s_tmd, u32 version) {
    tmd *p_tmd;
    p_tmd = (tmd*) SIGNATURE_PAYLOAD(s_tmd);
    p_tmd->title_version = version;
    tmd_dirty = 1;
}

void change_tmd_title_id(signed_blob *s_tmd, u32 titleid1, u32 titleid2) {
    tmd *p_tmd;
    u64 title_id = titleid1;
    title_id <<= 32;
    title_id |= titleid2;
    p_tmd = (tmd*) SIGNATURE_PAYLOAD(s_tmd);
    p_tmd->title_id = title_id;
    tmd_dirty = 1;
}

void display_tag(u8 *buf) {
    gprintf("Firmware version: %s      Builder: %s", buf, buf+0x30);
}

void display_ios_tags(u8 *buf, u32 size) {
    u32 i;
    char *ios_version_tag = "$IOSVersion:";
    char version_buf[128], *date;

    if (size == 64) {
        display_tag(buf);
        return;
    }
    
    for (i = 0; i < (size - 64); i++) {
        if (!strncmp((char *) buf + i, ios_version_tag, 10)) {
            while (buf[i + strlen(ios_version_tag)] == ' ')
                i++; // skip spaces
            strlcpy(version_buf, (char *) buf + i + strlen(ios_version_tag),
                sizeof version_buf);
            date = version_buf;
            strsep(&date, "$");
            date = version_buf;
            strsep(&date, ":");
            gprintf("%s (%s)\n", version_buf, date);
            i += 64;
        }
    }
}

void print_tmd_summary(const tmd *p_tmd) {
    const tmd_content *p_cr;
    p_cr = TMD_CONTENTS(p_tmd);
    u32 size = 0;
    u16 i;
    
    for (i = 0; i < p_tmd->num_contents; i++)
        size += p_cr[i].size;
    
    gprintf("Title ID: %016llx\n",p_tmd->title_id); gprintf("Title Version: %u", p_tmd->title_version); gprintf("Number of parts: %d.  Total size: %uK\n", p_tmd->num_contents, (u32) (size / 1024));
}

void zero_sig(signed_blob *sig) {
    u8 *sig_ptr = (u8 *) sig;
    memset(sig_ptr + 4, 0, SIGNATURE_SIZE(sig) - 4);
}

void brute_tmd(tmd *p_tmd) {
    u16 fill;
    for (fill = 0; fill < 65535; fill++) {
        p_tmd->fill3 = fill;
        sha1 hash;
        //gprintf("SHA1(%p, %x, %p)\n", p_tmd, TMD_SIZE(p_tmd), hash);
        SHA1((u8 *) p_tmd, TMD_SIZE(p_tmd), hash);
        ;
        
        if (hash[0] == 0) {
            gprintf("setting fill3 to %04hx\n", fill);
            return;
        }
    }
    printf("Unable to fix tmd :(\n");
    ReturnToLoader();
}

void brute_tik(tik *p_tik) {
    u16 fill;
    for (fill = 0; fill < 65535; fill++) {
        p_tik->padding = fill;
        sha1 hash;
        //    gprintf("SHA1(%p, %x, %p)\n", p_tmd, TMD_SIZE(p_tmd), hash);
        SHA1((u8 *) p_tik, sizeof(tik), hash);
        if (hash[0] == 0) return;
    }
    printf("Unable to fix tik :(\n");
    ReturnToLoader();
}

void forge_tmd(signed_blob *s_tmd) {
    gprintf("forging tmd sig\n");
    zero_sig(s_tmd);
    brute_tmd(SIGNATURE_PAYLOAD(s_tmd));
}

void forge_tik(signed_blob *s_tik) {
    gprintf("forging tik sig\n");
    zero_sig(s_tik);
    brute_tik(SIGNATURE_PAYLOAD(s_tik));
}

int install_ticket(const signed_blob *s_tik, const signed_blob *s_certs,
    u32 certs_len) {
    u32 ret;

    //  gprintf("Installing ticket...\n");
    ret = ES_AddTicket(s_tik, STD_SIGNED_TIK_SIZE, s_certs, certs_len, NULL, 0);
    if (ret < 0) {
        gprintf("ES_AddTicket failed: %d\n",ret);
        return ret;
    }
    return 0;
}

s32 install(const signed_blob *s_tmd, const signed_blob *s_certs, u32 certs_len) {
    u32 ret, i;
    tmd *p_tmd = SIGNATURE_PAYLOAD(s_tmd);
    //  gprintf("Adding title...\n");

    ret = ES_AddTitleStart(s_tmd, SIGNED_TMD_SIZE(s_tmd), s_certs, certs_len,
        NULL, 0);
    if (ret < 0) {
        gprintf("ES_AddTitleStart failed: %d\n",ret);
        ES_AddTitleCancel();
        return ret;
    }

    for (i = 0; i < p_tmd->num_contents; i++) {
        //gprintf("Adding content ID %08x", i);
        printf("\b%u....", i + 1);
        ret = install_nus_object((tmd *) SIGNATURE_PAYLOAD(s_tmd), i);
        if (ret) return ret;
    }
    printf("\b!\n");
    
    ret = ES_AddTitleFinish();
    if (ret < 0) {
        printf("ES_AddTitleFinish failed: %d\n", ret);
        ES_AddTitleCancel();
        return ret;
    }
    //  printf("Installation complete!\n");
    return 0;
}

void patchmii_download(u32 titleid1, u32 titleid2, u16 *version, bool patch,
    bool patch2) {
    u8 *temp_tmdbuf = NULL, *temp_tikbuf = NULL, *content_buf, *decrypted_buf;
    u32 content_size, tmdsize;
    u8 update_tmd;
    u8 key[16];
    const tmd *p_tmd;
    tmd_content *p_cr;
    sha1 hash;
    static char cidstr[32];
    char tmdstring[20] = "tmd";
    int i, retval;
    //gprintf("\npatchmii_download() useSd = %d",useSd);

    if (ISFS_Initialize() || create_temp_dir()) {
        gcprintf("Failed to create temp dir: ");
        ReturnToLoader();
    }

    char tempstr[80];
    if (*version != 0) {
        sprintf(tempstr, "%s.%u", tmdstring, *version);
        sprintf(tmdstring, "%s", tempstr);
    }

    printf("TMD...");
    SpinnerStart();
    retval = GetNusObject(titleid1, titleid2, version, tmdstring, &temp_tmdbuf,
        &tmdsize);
    SpinnerStop();
    if (retval < 0) {
        gprintf("GetNusObject(tmd) returned %d, tmdsize = %u\n", retval, tmdsize);
        ReturnToLoader();
    }
    if (temp_tmdbuf == NULL) {
        gprintf("Failed to allocate temp buffer for encrypted content, size was %u\n", tmdsize);
        ReturnToLoader();
    }
    memcpy(tmdbuf, temp_tmdbuf, MIN(tmdsize, sizeof(tmdbuf)));
    free(temp_tmdbuf);
    
    s_tmd = (signed_blob *) tmdbuf;
    if (!IS_VALID_SIGNATURE(s_tmd)) {
        gprintf("Bad TMD signature!\n");
        ReturnToLoader();
    }
    printf("\b.Done\n");
    
    printf("Ticket...");
    u32 ticketsize;
    
    SpinnerStart();
    retval = GetNusObject(titleid1, titleid2, version, "cetk", &temp_tikbuf,
        &ticketsize);
    SpinnerStop();
    
    if (retval < 0) gprintf("GetNusObject(cetk) returned %d, ticketsize = %u\n", retval, ticketsize);
    memcpy(tikbuf, temp_tikbuf, MIN(ticketsize, sizeof(tikbuf)));
    
    s_tik = (signed_blob *) tikbuf;
    if (!IS_VALID_SIGNATURE(s_tik)) {
        gprintf("Bad tik signature!\n");
        ReturnToLoader();
    }

    free(temp_tikbuf);
    
    printf("\b.Done\n");
    
    s_certs = (signed_blob *) cert_sys;
    if (!IS_VALID_SIGNATURE(s_certs)) {
        printf("Bad cert signature!\n");
        ReturnToLoader();
    }

    get_title_key(s_tik, key);
    aes_set_key(key);
    
    p_tmd = (tmd*) SIGNATURE_PAYLOAD(s_tmd);
    p_cr = TMD_CONTENTS((tmd*) p_tmd);
    
    //  print_tmd_summary(p_tmd);
    //  gprintf("\b ..games..\b");

    for (i = 0; i < p_tmd->num_contents; i++) {
        //      gprintf("Downloading part %d/%d (%lluK): ", i+1,
        //                  p_tmd->num_contents, p_cr[i].size / 1024);
        sprintf(cidstr, "%08x", p_cr[i].cid);
        
        printf("\bContent %u/%u ID: %08x....", i + 1, p_tmd->num_contents,
            p_cr[i].cid);
        SpinnerStart();
        retval = GetNusObject(titleid1, titleid2, version, cidstr,
            &content_buf, &content_size);
        SpinnerStop();
        if (retval < 0) {
            gprintf("GetNusObject(%s) failed with error %d, content size = %u\n", cidstr, retval, content_size);
            ReturnToLoader();
        }

        if (content_buf == NULL) {
            gprintf("error allocating content buffer, size was %u\n", content_size);
            ReturnToLoader();
        }

        if (content_size % 16) {
            gprintf("ERROR: downloaded content[%d] size %u is not a multiple of 16\n", i, content_size);
            free(content_buf);
            ReturnToLoader();
        }

        if (content_size < p_cr[i].size) {
            gprintf("ERROR: only downloaded %u / %llu bytes\n", content_size, p_cr[i].size);
            free(content_buf);
            ReturnToLoader();
        }

        decrypted_buf = malloc(content_size);
        if (!decrypted_buf) {
            gprintf("ERROR: failed to allocate decrypted_buf (%u bytes)\n", content_size);
            free(content_buf);
            ReturnToLoader();
        }

        decrypt_buffer(i, content_buf, decrypted_buf, content_size);
        printf("\b.Done\n");
        
        SHA1(decrypted_buf, p_cr[i].size, hash);
        
        if (!memcmp(p_cr[i].hash, hash, sizeof hash)) {
            //          gprintf("\b\b hash OK. ");
            //          display_ios_tags(decrypted_buf, content_size);
            update_tmd = 0;
            if (patch) {
                if ((p_tmd->title_id >> 32) == 1 && ((u32)(p_tmd->title_id))
                        > 2) {
                    if (patch_fake_sign_check(decrypted_buf, content_size)) update_tmd
                            = 1;
                }
            }
            if (patch2) {
                if ((p_tmd->title_id >> 32) == 1 && ((u32)(p_tmd->title_id))
                        > 2) {
                    if (patch_identify_check(decrypted_buf, content_size)) update_tmd
                            = 1;
                }
            }
            if (update_tmd == 1) {
                // gprintf("Updating TMD.\n");
                SHA1(decrypted_buf, p_cr[i].size, hash);
                memcpy(p_cr[i].hash, hash, sizeof hash);
                if (p_cr[i].type == 0x8001) p_cr[i].type = 1;
                tmd_dirty = 1;
            }

            retval = (int) save_nus_object(p_cr[i].cid, decrypted_buf,
                content_size);
            if (retval < 0) {
                printf("save_nus_object(%x) returned error %d\n", p_cr[i].cid,
                    retval);
                ReturnToLoader();
            }
        } else {
            printf("hash BAD\n");
            ReturnToLoader();
        }

        free(decrypted_buf);
        free(content_buf);
    }
    //  gprintf("\b ..keys..\b");
}

s32 patchmii_install(u32 in_title_h, u32 in_title_l, u16 in_version,
    u32 out_title_h, u32 out_title_l, u16 out_version, bool patch, bool patch2) {
    u16 version = in_version;
    //gprintf("\npatchmii_install()");
    if (version) {
        gcprintf("Downloading Title %08x-%08x v%u.....\n", in_title_h,
            in_title_l, version);
    } else {
        gcprintf("Downloading Title %08x-%08x.....\n", in_title_h, in_title_l);
    }

    SpinnerStart();
    patchmii_download(in_title_h, in_title_l, &version, patch, patch2);
    SpinnerStop();
    
    if (in_version != 0) {
        if (in_title_h != out_title_h || in_title_l != out_title_l) {
            change_ticket_title_id(s_tik, out_title_h, out_title_l);
            change_tmd_title_id(s_tmd, out_title_h, out_title_l);
            tmd_dirty = 1;
            tik_dirty = 1;
        }

        if (version != out_version) {
            change_tmd_version(s_tmd, out_version);
            tmd_dirty = 1;
            tik_dirty = 1;
        }

        if (tmd_dirty) {
            forge_tmd(s_tmd);
            tmd_dirty = 0;
        }
        if (tik_dirty) {
            forge_tik(s_tik);
            tik_dirty = 0;
        }
    }

    if (out_version) {
        gcprintf("\bDownload complete. Installing to Title %08x-%08x v%u...\n",
            out_title_h, out_title_l, out_version);
    } else {
        gcprintf("\bDownload complete. Installing to Title %08x-%08x...\n",
            out_title_h, out_title_l);
    }

    int retval = install_ticket(s_tik, s_certs, cert_sys_size);
    if (retval) {
        gprintf("install_ticket returned %d\n", retval);
        ReturnToLoader();
    }

    SpinnerStart();
    retval = install(s_tmd, s_certs, cert_sys_size);
    SpinnerStop();
    //  gprintf("\b..hacks..\b");

    if (retval) {
        gprintf("\b\ninstall returned %d\n", retval);
    } else {
        printf("\bInstallation complete!\n");
    }
    return retval;
}
