/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "signtool.h"
#include "zip.h"
#include "prmem.h"
#include "blapi.h"
#include "sechash.h" /* for HASH_GetHashObject() */

static int create_pk7(char *dir, char *keyName, int *keyType);
static int jar_find_key_type(CERTCertificate *cert);
static int manifesto(char *dirname, char *install_script, PRBool recurse);
static int manifesto_fn(char *relpath, char *basedir, char *reldir,
                        char *filename, void *arg);
static int manifesto_xpi_fn(char *relpath, char *basedir, char *reldir,
                            char *filename, void *arg);
static int sign_all_arc_fn(char *relpath, char *basedir, char *reldir,
                           char *filename, void *arg);
static int add_meta(FILE *fp, char *name);
static int SignFile(FILE *outFile, FILE *inFile, CERTCertificate *cert);
static int generate_SF_file(char *manifile, char *who);
static int calculate_MD5_range(FILE *fp, long r1, long r2,
                               JAR_Digest *dig);
static void SignOut(void *arg, const char *buf, unsigned long len);

static char *metafile = NULL;
static int optimize = 0;
static FILE *mf;
static ZIPfile *zipfile = NULL;

/*
 *  S i g n A r c h i v e
 *
 *  Sign an individual archive tree. A directory
 *  called META-INF is created underneath this.
 *
 */
int
SignArchive(char *tree, char *keyName, char *zip_file, int javascript,
            char *meta_file, char *install_script, int _optimize, PRBool recurse)
{
    int status;
    char tempfn[FNSIZE], fullfn[FNSIZE];
    int keyType = rsaKey;

    metafile = meta_file;
    optimize = _optimize;

    /* To create XPI compatible Archive manifesto() must be run before
     * the zipfile is opened. This is so the signed files are not added
     * the archive before the crucial rsa/dsa file*/
    if (xpi_arc) {
        manifesto(tree, install_script, recurse);
    }

    if (zip_file) {
        zipfile = JzipOpen(zip_file, NULL /*no comment*/);
    }

    /*Sign and add files to the archive normally with manifesto()*/
    if (!xpi_arc) {
        manifesto(tree, install_script, recurse);
    }

    if (keyName) {
        status = create_pk7(tree, keyName, &keyType);
        if (status < 0) {
            PR_fprintf(errorFD, "the tree \"%s\" was NOT SUCCESSFULLY SIGNED\n",
                       tree);
            errorCount++;
            exit(ERRX);
        }
    }

    /* Add the rsa/dsa file as the first file in the archive. This is crucial
     * for a XPInstall compatible archive */
    if (xpi_arc) {
        if (verbosity >= 0) {
            PR_fprintf(outputFD, "%s \n", XPI_TEXT);
        }

        /* rsa/dsa to zip */
        sprintf(tempfn, "META-INF/%s.%s", base, (keyType == dsaKey ? "dsa"
                                                                   : "rsa"));
        sprintf(fullfn, "%s/%s", tree, tempfn);
        JzipAdd(fullfn, tempfn, zipfile, compression_level);

        /* Loop through all files & subdirectories, add to archive */
        foreach (tree, "", manifesto_xpi_fn, recurse, PR_FALSE /*include dirs */,
                 (void *)NULL)
            ;
    }
    /* mf to zip */
    strcpy(tempfn, "META-INF/manifest.mf");
    sprintf(fullfn, "%s/%s", tree, tempfn);
    JzipAdd(fullfn, tempfn, zipfile, compression_level);

    /* sf to zip */
    sprintf(tempfn, "META-INF/%s.sf", base);
    sprintf(fullfn, "%s/%s", tree, tempfn);
    JzipAdd(fullfn, tempfn, zipfile, compression_level);

    /* Add the rsa/dsa file to the zip archive normally */
    if (!xpi_arc) {
        /* rsa/dsa to zip */
        sprintf(tempfn, "META-INF/%s.%s", base, (keyType == dsaKey ? "dsa"
                                                                   : "rsa"));
        sprintf(fullfn, "%s/%s", tree, tempfn);
        JzipAdd(fullfn, tempfn, zipfile, compression_level);
    }

    JzipClose(zipfile);

    if (verbosity >= 0) {
        if (javascript) {
            PR_fprintf(outputFD, "jarfile \"%s\" signed successfully\n",
                       zip_file);
        } else {
            PR_fprintf(outputFD, "tree \"%s\" signed successfully\n",
                       tree);
        }
    }

    return 0;
}

typedef struct {
    char *keyName;
    int javascript;
    char *metafile;
    char *install_script;
    int optimize;
} SignArcInfo;

/*
 *  S i g n A l l A r c
 *
 *  Javascript may generate multiple .arc directories, one
 *  for each jar archive needed. Sign them all.
 *
 */
int
SignAllArc(char *jartree, char *keyName, int javascript, char *metafile,
           char *install_script, int optimize, PRBool recurse)
{
    SignArcInfo info;

    info.keyName = keyName;
    info.javascript = javascript;
    info.metafile = metafile;
    info.install_script = install_script;
    info.optimize = optimize;

    return foreach (jartree, "", sign_all_arc_fn, recurse,
                    PR_TRUE /*include dirs*/, (void *)&info);
}

static int
sign_all_arc_fn(char *relpath, char *basedir, char *reldir, char *filename,
                void *arg)
{
    char *zipfile = NULL;
    char *arc = NULL, *archive = NULL;
    int retval = 0;
    SignArcInfo *infop = (SignArcInfo *)arg;

    /* Make sure there is one and only one ".arc" in the relative path,
     * and that it is at the end of the path (don't sign .arcs within .arcs) */
    if ((PL_strcaserstr(relpath, ".arc") == relpath + strlen(relpath) - 4) &&
        (PL_strcasestr(relpath, ".arc") == relpath + strlen(relpath) - 4)) {

        if (!infop) {
            PR_fprintf(errorFD, "%s: Internal failure\n", PROGRAM_NAME);
            errorCount++;
            retval = -1;
            goto finish;
        }
        archive = PR_smprintf("%s/%s", basedir, relpath);

        zipfile = PL_strdup(archive);
        arc = PORT_Strrchr(zipfile, '.');

        if (arc == NULL) {
            PR_fprintf(errorFD, "%s: Internal failure\n", PROGRAM_NAME);
            errorCount++;
            retval = -1;
            goto finish;
        }

        PL_strcpy(arc, ".jar");

        if (verbosity >= 0) {
            PR_fprintf(outputFD, "\nsigning: %s\n", zipfile);
        }
        retval = SignArchive(archive, infop->keyName, zipfile,
                             infop->javascript, infop->metafile, infop->install_script,
                             infop->optimize, PR_TRUE /* recurse */);
    }
finish:
    if (archive)
        PR_Free(archive);
    if (zipfile)
        PR_Free(zipfile);

    return retval;
}

/*********************************************************************
 *
 * c r e a t e _ p k 7
 */
static int
create_pk7(char *dir, char *keyName, int *keyType)
{
    int status = 0;
    char *file_ext;

    CERTCertificate *cert;
    CERTCertDBHandle *db;

    FILE *in, *out;

    char sf_file[FNSIZE];
    char pk7_file[FNSIZE];

    /* open cert database */
    db = CERT_GetDefaultCertDB();

    if (db == NULL)
        return -1;

    /* find cert */
    /*cert = CERT_FindCertByNicknameOrEmailAddr(db, keyName);*/
    cert = PK11_FindCertFromNickname(keyName, &pwdata);

    if (cert == NULL) {
        SECU_PrintError(PROGRAM_NAME,
                        "Cannot find the cert \"%s\"", keyName);
        return -1;
    }

    /* determine the key type, which sets the extension for pkcs7 object */

    *keyType = jar_find_key_type(cert);
    file_ext = (*keyType == dsaKey) ? "dsa" : "rsa";

    sprintf(sf_file, "%s/META-INF/%s.sf", dir, base);
    sprintf(pk7_file, "%s/META-INF/%s.%s", dir, base, file_ext);

    if ((in = fopen(sf_file, "rb")) == NULL) {
        PR_fprintf(errorFD, "%s: Can't open %s for reading\n", PROGRAM_NAME,
                   sf_file);
        errorCount++;
        exit(ERRX);
    }

    if ((out = fopen(pk7_file, "wb")) == NULL) {
        PR_fprintf(errorFD, "%s: Can't open %s for writing\n", PROGRAM_NAME,
                   sf_file);
        errorCount++;
        exit(ERRX);
    }

    status = SignFile(out, in, cert);

    CERT_DestroyCertificate(cert);
    fclose(in);
    fclose(out);

    if (status) {
        PR_fprintf(errorFD, "%s: PROBLEM signing data (%s)\n",
                   PROGRAM_NAME, SECU_Strerror(PORT_GetError()));
        errorCount++;
        return -1;
    }

    return 0;
}

/*
 *  j a r _ f i n d _ k e y _ t y p e
 *
 *  Determine the key type for a given cert, which
 * should be rsaKey or dsaKey. Any error return 0.
 *
 */
static int
jar_find_key_type(CERTCertificate *cert)
{
    SECKEYPrivateKey *privk = NULL;
    KeyType keyType;

    /* determine its type */
    privk = PK11_FindKeyByAnyCert(cert, &pwdata);
    if (privk == NULL) {
        PR_fprintf(errorFD, "warning - can't find private key for this cert\n");
        warningCount++;
        return 0;
    }

    keyType = privk->keyType;
    SECKEY_DestroyPrivateKey(privk);
    return keyType;
}

/*
 *  m a n i f e s t o
 *
 *  Run once for every subdirectory in which a
 *  manifest is to be created -- usually exactly once.
 *
 */
static int
manifesto(char *dirname, char *install_script, PRBool recurse)
{
    char metadir[FNSIZE], sfname[FNSIZE];

    /* Create the META-INF directory to hold signing info */

    if (PR_Access(dirname, PR_ACCESS_READ_OK)) {
        PR_fprintf(errorFD, "%s: unable to read your directory: %s\n",
                   PROGRAM_NAME, dirname);
        errorCount++;
        perror(dirname);
        exit(ERRX);
    }

    if (PR_Access(dirname, PR_ACCESS_WRITE_OK)) {
        PR_fprintf(errorFD, "%s: unable to write to your directory: %s\n",
                   PROGRAM_NAME, dirname);
        errorCount++;
        perror(dirname);
        exit(ERRX);
    }

    sprintf(metadir, "%s/META-INF", dirname);

    strcpy(sfname, metadir);

    PR_MkDir(metadir, 0777);

    strcat(metadir, "/");
    strcat(metadir, MANIFEST);

    if ((mf = fopen(metadir, "wb")) == NULL) {
        perror(MANIFEST);
        PR_fprintf(errorFD, "%s: Probably, the directory you are trying to"
                            " sign has\n",
                   PROGRAM_NAME);
        PR_fprintf(errorFD, "%s: permissions problems or may not exist.\n",
                   PROGRAM_NAME);
        errorCount++;
        exit(ERRX);
    }

    if (verbosity >= 0) {
        PR_fprintf(outputFD, "Generating %s file..\n", metadir);
    }

    fprintf(mf, "Manifest-Version: 1.0\n");
    fprintf(mf, "Created-By: %s\n", CREATOR);
    fprintf(mf, "Comments: %s\n", BREAKAGE);

    if (scriptdir) {
        fprintf(mf, "Comments: --\n");
        fprintf(mf, "Comments: --\n");
        fprintf(mf, "Comments: -- This archive signs Javascripts which may not necessarily\n");
        fprintf(mf, "Comments: -- be included in the physical jar file.\n");
        fprintf(mf, "Comments: --\n");
        fprintf(mf, "Comments: --\n");
    }

    if (install_script)
        fprintf(mf, "Install-Script: %s\n", install_script);

    if (metafile)
        add_meta(mf, "+");

    /* Loop through all files & subdirectories */
    foreach (dirname, "", manifesto_fn, recurse, PR_FALSE /*include dirs */,
             (void *)NULL)
        ;

    fclose(mf);

    strcat(sfname, "/");
    strcat(sfname, base);
    strcat(sfname, ".sf");

    if (verbosity >= 0) {
        PR_fprintf(outputFD, "Generating %s.sf file..\n", base);
    }
    generate_SF_file(metadir, sfname);

    return 0;
}

/*
 *  m a n i f e s t o _ x p i _ f n
 *
 *  Called by pointer from SignArchive(), once for
 *  each file within the directory. This function
 *  is only used for adding to XPI compatible archive
 *
 */
static int
manifesto_xpi_fn(char *relpath, char *basedir, char *reldir, char *filename, void *arg)
{
    char fullname[FNSIZE];

    if (verbosity >= 0) {
        PR_fprintf(outputFD, "--> %s\n", relpath);
    }

    /* extension matching */
    if (extensionsGiven) {
        char *ext = PL_strrchr(relpath, '.');
        if (!ext)
            return 0;
        if (!PL_HashTableLookup(extensions, ext))
            return 0;
    }
    sprintf(fullname, "%s/%s", basedir, relpath);
    JzipAdd(fullname, relpath, zipfile, compression_level);

    return 0;
}

/*
 *  m a n i f e s t o _ f n
 *
 *  Called by pointer from manifesto(), once for
 *  each file within the directory.
 *
 */
static int
manifesto_fn(char *relpath, char *basedir, char *reldir, char *filename, void *arg)
{
    int use_js;
    char *md5, *sha1;

    JAR_Digest dig;
    char fullname[FNSIZE];

    if (verbosity >= 0) {
        PR_fprintf(outputFD, "--> %s\n", relpath);
    }

    /* extension matching */
    if (extensionsGiven) {
        char *ext = PL_strrchr(relpath, '.');
        if (!ext)
            return 0;
        if (!PL_HashTableLookup(extensions, ext))
            return 0;
    }

    sprintf(fullname, "%s/%s", basedir, relpath);

    fprintf(mf, "\n");

    use_js = 0;

    if (scriptdir && !PORT_Strcmp(scriptdir, reldir))
        use_js++;

    /* sign non-.js files inside .arc directories using the javascript magic */

    if ((PL_strcaserstr(filename, ".js") != filename + strlen(filename) - 3) &&
        (PL_strcaserstr(reldir, ".arc") == reldir + strlen(filename) - 4))
        use_js++;

    if (use_js) {
        fprintf(mf, "Name: %s\n", filename);
        fprintf(mf, "Magic: javascript\n");

        if (optimize == 0)
            fprintf(mf, "javascript.id: %s\n", filename);

        if (metafile)
            add_meta(mf, filename);
    } else {
        fprintf(mf, "Name: %s\n", relpath);
        if (metafile)
            add_meta(mf, relpath);
    }

    JAR_digest_file(fullname, &dig);

    if (optimize == 0) {
        fprintf(mf, "Digest-Algorithms: MD5 SHA1\n");

        md5 = BTOA_DataToAscii(dig.md5, MD5_LENGTH);
        fprintf(mf, "MD5-Digest: %s\n", md5);
        PORT_Free(md5);
    }

    sha1 = BTOA_DataToAscii(dig.sha1, SHA1_LENGTH);
    fprintf(mf, "SHA1-Digest: %s\n", sha1);
    PORT_Free(sha1);

    if (!use_js) {
        JzipAdd(fullname, relpath, zipfile, compression_level);
    }

    return 0;
}

/*
 *  a d d _ m e t a
 *
 *  Parse the metainfo file, and add any details
 *  necessary to the manifest file. In most cases you
 *  should be using the -i option (ie, for SmartUpdate).
 *
 */
static int
add_meta(FILE *fp, char *name)
{
    FILE *met;
    char buf[BUFSIZ];

    int place;
    char *pattern, *meta;

    int num = 0;

    if ((met = fopen(metafile, "r")) != NULL) {
        while (fgets(buf, BUFSIZ, met)) {
            char *s;

            for (s = buf; *s && *s != '\n' && *s != '\r'; s++)
                ;
            *s = 0;

            if (*buf == 0)
                continue;

            pattern = buf;

            /* skip to whitespace */
            for (s = buf; *s && *s != ' ' && *s != '\t'; s++)
                ;

            /* terminate pattern */
            if (*s == ' ' || *s == '\t')
                *s++ = 0;

            /* eat through whitespace */
            while (*s == ' ' || *s == '\t')
                s++;

            meta = s;

            /* this will eventually be regexp matching */

            place = 0;
            if (!PORT_Strcmp(pattern, name))
                place = 1;

            if (place) {
                num++;
                if (verbosity >= 0) {
                    PR_fprintf(outputFD, "[%s] %s\n", name, meta);
                }
                fprintf(fp, "%s\n", meta);
            }
        }
        fclose(met);
    } else {
        PR_fprintf(errorFD, "%s: can't open metafile: %s\n", PROGRAM_NAME,
                   metafile);
        errorCount++;
        exit(ERRX);
    }

    return num;
}

/**********************************************************************
 *
 * S i g n F i l e
 */
static int
SignFile(FILE *outFile, FILE *inFile, CERTCertificate *cert)
{
    int nb;
    char ibuf[4096], digestdata[32];
    const SECHashObject *hashObj;
    void *hashcx;
    unsigned int len;

    SECItem digest;
    SEC_PKCS7ContentInfo *cinfo;
    SECStatus rv;

    if (outFile == NULL || inFile == NULL || cert == NULL)
        return -1;

    /* XXX probably want to extend interface to allow other hash algorithms */
    hashObj = HASH_GetHashObject(HASH_AlgSHA1);

    hashcx = (*hashObj->create)();
    if (hashcx == NULL)
        return -1;

    (*hashObj->begin)(hashcx);

    for (;;) {
        if (feof(inFile))
            break;
        nb = fread(ibuf, 1, sizeof(ibuf), inFile);
        if (nb == 0) {
            if (ferror(inFile)) {
                PORT_SetError(SEC_ERROR_IO);
                (*hashObj->destroy)(hashcx, PR_TRUE);
                return -1;
            }
            /* eof */
            break;
        }
        (*hashObj->update)(hashcx, (unsigned char *)ibuf, nb);
    }

    (*hashObj->end)(hashcx, (unsigned char *)digestdata, &len, 32);
    (*hashObj->destroy)(hashcx, PR_TRUE);

    digest.data = (unsigned char *)digestdata;
    digest.len = len;

    cinfo = SEC_PKCS7CreateSignedData(cert, certUsageObjectSigner, NULL,
                                      SEC_OID_SHA1, &digest, NULL, NULL);

    if (cinfo == NULL)
        return -1;

    rv = SEC_PKCS7IncludeCertChain(cinfo, NULL);
    if (rv != SECSuccess) {
        SEC_PKCS7DestroyContentInfo(cinfo);
        return -1;
    }

    if (no_time == 0) {
        rv = SEC_PKCS7AddSigningTime(cinfo);
        if (rv != SECSuccess) {
            /* don't check error */
        }
    }

    rv = SEC_PKCS7Encode(cinfo, SignOut, outFile, NULL, NULL, &pwdata);

    SEC_PKCS7DestroyContentInfo(cinfo);

    if (rv != SECSuccess)
        return -1;

    return 0;
}

/*
 *  g e n e r a t e _ S F _ f i l e
 *
 *  From the supplied manifest file, calculates
 *  digests on the various sections, creating a .SF
 *  file in the process.
 *
 */
static int
generate_SF_file(char *manifile, char *who)
{
    FILE *sf;
    FILE *mf;
    long r1, r2, r3;
    char whofile[FNSIZE];
    char *buf, *name = NULL;
    char *md5, *sha1;
    JAR_Digest dig;
    int line = 0;

    strcpy(whofile, who);

    if ((mf = fopen(manifile, "rb")) == NULL) {
        perror(manifile);
        exit(ERRX);
    }

    if ((sf = fopen(whofile, "wb")) == NULL) {
        perror(who);
        exit(ERRX);
    }

    buf = (char *)PORT_ZAlloc(BUFSIZ);

    if (buf)
        name = (char *)PORT_ZAlloc(BUFSIZ);

    if (buf == NULL || name == NULL)
        out_of_memory();

    fprintf(sf, "Signature-Version: 1.0\n");
    fprintf(sf, "Created-By: %s\n", CREATOR);
    fprintf(sf, "Comments: %s\n", BREAKAGE);

    if (fgets(buf, BUFSIZ, mf) == NULL) {
        PR_fprintf(errorFD, "%s: empty manifest file!\n", PROGRAM_NAME);
        errorCount++;
        exit(ERRX);
    }

    if (strncmp(buf, "Manifest-Version:", 17)) {
        PR_fprintf(errorFD, "%s: not a manifest file!\n", PROGRAM_NAME);
        errorCount++;
        exit(ERRX);
    }

    fseek(mf, 0L, SEEK_SET);

    /* Process blocks of headers, and calculate their hashen */

    while (1) {
        /* Beginning range */
        r1 = ftell(mf);

        if (fgets(name, BUFSIZ, mf) == NULL)
            break;

        line++;

        if (r1 != 0 && strncmp(name, "Name:", 5)) {
            PR_fprintf(errorFD,
                       "warning: unexpected input in manifest file \"%s\" at line %d:\n",
                       manifile, line);
            PR_fprintf(errorFD, "%s\n", name);
            warningCount++;
        }

        r2 = r1;
        while (fgets(buf, BUFSIZ, mf)) {
            if (*buf == 0 || *buf == '\n' || *buf == '\r')
                break;

            line++;

            /* Ending range for hashing */
            r2 = ftell(mf);
        }

        r3 = ftell(mf);

        if (r1) {
            fprintf(sf, "\n");
            fprintf(sf, "%s", name);
        }

        calculate_MD5_range(mf, r1, r2, &dig);

        if (optimize == 0) {
            fprintf(sf, "Digest-Algorithms: MD5 SHA1\n");

            md5 = BTOA_DataToAscii(dig.md5, MD5_LENGTH);
            fprintf(sf, "MD5-Digest: %s\n", md5);
            PORT_Free(md5);
        }

        sha1 = BTOA_DataToAscii(dig.sha1, SHA1_LENGTH);
        fprintf(sf, "SHA1-Digest: %s\n", sha1);
        PORT_Free(sha1);

        /* restore normalcy after changing offset position */
        fseek(mf, r3, SEEK_SET);
    }

    PORT_Free(buf);
    PORT_Free(name);

    fclose(sf);
    fclose(mf);

    return 0;
}

/*
 *  c a l c u l a t e _ M D 5 _ r a n g e
 *
 *  Calculate the MD5 digest on a range of bytes in
 *  the specified fopen'd file. Returns base64.
 *
 */
static int
calculate_MD5_range(FILE *fp, long r1, long r2, JAR_Digest *dig)
{
    int num;
    int range;
    unsigned char *buf;
    SECStatus rv;

    range = r2 - r1;

    /* position to the beginning of range */
    fseek(fp, r1, SEEK_SET);

    buf = (unsigned char *)PORT_ZAlloc(range);
    if (buf == NULL)
        out_of_memory();

    if ((num = fread(buf, 1, range, fp)) != range) {
        PR_fprintf(errorFD, "%s: expected %d bytes, got %d\n", PROGRAM_NAME,
                   range, num);
        errorCount++;
        exit(ERRX);
    }

    rv = PK11_HashBuf(SEC_OID_MD5, dig->md5, buf, range);
    if (rv == SECSuccess) {
        rv = PK11_HashBuf(SEC_OID_SHA1, dig->sha1, buf, range);
    }
    if (rv != SECSuccess) {
        PR_fprintf(errorFD, "%s: can't generate digest context\n",
                   PROGRAM_NAME);
        errorCount++;
        exit(ERRX);
    }

    PORT_Free(buf);

    return 0;
}

static void
SignOut(void *arg, const char *buf, unsigned long len)
{
    fwrite(buf, len, 1, (FILE *)arg);
}