/*
 * RabbitSign - Tools for signing TI graphing calculator software
 * Copyright (C) 2009 Benjamin Moody
 *
 * 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; either version 3 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <stdio.h>

#include "rabbitsign.h"
#include "internal.h"
#include "md5.h"

/*
 * Check/fix app/OS header and data.
 *
 * (This is something of a work in progress; a lot more
 * experimentation would be useful to determine what exactly is
 * required of app and OS headers on the 68k calculators.)
 */
static int repair_app(RSProgram* app,     /* app to repair */
		      unsigned int flags, /* flags */
		      unsigned int type)  /* field type */
{
  unsigned long length, hdrstart, hdrsize, fieldhead,
    fieldstart, fieldsize;
  unsigned char *hdr;
  int e;

  if (app->length < 6
      || app->data[0] != type
      || (app->data[1] & 0xf0) != 0) {
    rs_error(NULL, app, "no app header found");
    return RS_ERR_MISSING_HEADER;
  }

  /* Determine application length */

  length = app->length;
  rs_get_field_size(app->data, &hdrstart, &hdrsize);

  /* If requested, remove the old signature (truncate the application
     to its stated length.) */

  if (flags & RS_REMOVE_OLD_SIGNATURE) {
    if (length < hdrstart + hdrsize) {
      rs_warning(NULL, app, "provided app data too short");
    }
    else {
      if (length > hdrstart + hdrsize + 67)
	rs_warning(NULL, app, "re-signing discards %lu bytes",
		   length - hdrstart - hdrsize);
      length = hdrstart + hdrsize;
    }
  }
  else if (hdrsize && hdrstart + hdrsize != length) {
    rs_warning(NULL, app, "application length incorrect");
    rs_warning(NULL, app, "(perhaps you meant to use -r?)");
  }

  if ((e = rs_program_set_length(app, length)))
    return e;

  /* Set app size header to the correct value */

  hdrsize = length - hdrstart;
  if (rs_set_field_size(app->data, hdrsize)) {
    if (flags & RS_IGNORE_ALL_WARNINGS)
      rs_warning(NULL, app, "cannot set application length");
    else {
      rs_error(NULL, app, "cannot set application length");
      return RS_ERR_FIELD_TOO_SMALL;
    }
  }

  /* Check for key ID */

  hdr = app->data + hdrstart;
  if (hdrsize > 128)
    hdrsize = 128;

  if (rs_find_app_field((type << 8) | 0x10, hdr, hdrsize,
			NULL, NULL, NULL)) {
    if (flags & RS_IGNORE_ALL_WARNINGS)
      rs_warning(NULL, app, "application has no key ID");
    else {
      rs_error(NULL, app, "application has no key ID");
      return RS_ERR_MISSING_KEY_ID;
    }
  }

  /* Check for date stamp (note: I haven't actually tested whether
     this is required, but it always seems to be present in both 68k
     apps and OSes, and it is required for TI-83+ apps) */

  if (rs_find_app_field(0x0320, hdr, hdrsize,
			NULL, &fieldstart, &fieldsize)) {
    if (flags & RS_IGNORE_ALL_WARNINGS)
      rs_warning(NULL, app, "application has no date stamp");
    else {
      rs_error(NULL, app, "application has no date stamp");
      return RS_ERR_MISSING_DATE_STAMP;
    }
  }
  else if (rs_find_app_field(0x0900, hdr + fieldstart, fieldsize,
			NULL, NULL, NULL)) {
    if (flags & RS_IGNORE_ALL_WARNINGS)
      rs_warning(NULL, app, "application has no date stamp");
    else {
      rs_error(NULL, app, "application has no date stamp");
      return RS_ERR_MISSING_DATE_STAMP;
    }
  }
  else if (hdr[fieldstart + fieldsize] != 0x02
      || (hdr[fieldstart + fieldsize + 1] & 0xf0) != 0) {
    if (flags & RS_IGNORE_ALL_WARNINGS)
      rs_warning(NULL, app, "application has no date stamp signature");
    else {
      rs_error(NULL, app, "application has no date stamp signature");
      return RS_ERR_MISSING_DATE_STAMP;
    }
  }

  /* Check for program image field and fix length */

  if (rs_find_app_field((type << 8) | 0x70, hdr, hdrsize,
			&fieldhead, &fieldstart, &fieldsize)) {
    if (flags & RS_IGNORE_ALL_WARNINGS)
      rs_warning(NULL, app, "application has no program image field");
    else {
      rs_error(NULL, app, "application has no program image field");
      return RS_ERR_MISSING_PROGRAM_IMAGE;
    }
  }
  else {
    if ((fieldstart + hdrstart) % 2) {
      /* The OS appears to align apps so the start of the app header
	 is at an even address; if the application code itself is at
	 an odd address, bad stuff will happen. */
      if (flags & RS_IGNORE_ALL_WARNINGS)
	rs_warning(NULL, app, "application header is not a multiple of 2 bytes");
      else {
	rs_error(NULL, app, "application header is not a multiple of 2 bytes");
	return RS_ERR_MISALIGNED_PROGRAM_IMAGE;
      }
    }

    if (fieldsize && fieldstart + fieldsize != length - hdrstart)
      rs_warning(NULL, app, "program image length incorrect");

    if (rs_set_field_size(hdr + fieldhead, length - hdrstart - fieldstart)) {
      rs_error(NULL, app, "cannot set program image length");
      return RS_ERR_FIELD_TOO_SMALL;
    }
  }

  return RS_SUCCESS;
}

/*
 * Check/fix Flash app header and data.
 */
int rs_repair_ti9x_app(RSProgram* app,     /* app to repair */
		       unsigned int flags) /* flags */
{
  return repair_app(app, flags, 0x81);
}

/*
 * Check/fix OS header and data.
 */
int rs_repair_ti9x_os(RSProgram* app,     /* app to repair */
		      unsigned int flags) /* flags */
{
  return repair_app(app, flags, 0x80);
}

/*
 * Compute signature for a 68k app/OS.
 *
 * The app header should be checked and/or repaired by
 * rs_repair_ti9x_app() prior to calling this function.
 */
int rs_sign_ti9x_app(RSProgram* app, /* app to sign */
		     RSKey* key)     /* signing key */
{
  md5_uint32 hash[4];
  mpz_t hashv, sigv;
  unsigned char sigdata[512];
  size_t siglength;
  int e;

  md5_buffer((char*) app->data, app->length, &hash);

  mpz_init(hashv);
  mpz_init(sigv);

  mpz_import(hashv, 16, -1, 1, 0, 0, hash);
  rs_message(2, NULL, app, "hash = %ZX", hashv);

  if ((e = rs_sign_rsa(sigv, hashv, key))) {
    mpz_clear(hashv);
    mpz_clear(sigv);
    return e;
  }

  rs_message(2, NULL, app, "sig = %ZX", sigv);

  sigdata[0] = 0x02;
  sigdata[1] = 0x0d;
  mpz_export(sigdata + 3, &siglength, -1, 1, 0, 0, sigv);
  sigdata[2] = siglength & 0xff;
  siglength += 3;

  return rs_program_append_data(app, sigdata, siglength);
}

/*
 * Validate app/OS signature.
 */
int rs_validate_ti9x_app(const RSProgram* app, /* app to validate */
			 const RSKey* key)     /* signing key */
{
  unsigned long length, hdrstart, hdrsize, fieldstart, fieldsize;
  const unsigned char *hdr, *sig;
  md5_uint32 hash[4];
  mpz_t hashv, sigv;
  int e, e2 = RS_SUCCESS;

  if (app->length < 6) {
    rs_error(NULL, app, "no app header found");
    return RS_ERR_MISSING_HEADER;
  }

  rs_get_field_size(app->data, &hdrstart, &hdrsize);
  length = hdrstart + hdrsize;
  hdr = app->data + hdrstart;
  if (hdrsize > 128)
    hdrsize = 128;

  if (length + 4 > app->length || length + 67 < app->length) {
    rs_error(NULL, app, "incorrect application length");
    return RS_ERR_INCORRECT_PROGRAM_SIZE;
  }

  if (rs_find_app_field((app->data[0] << 8) | 0x70, hdr, hdrsize,
			NULL, &fieldstart, &fieldsize)) {
    rs_warning(NULL, app, "application has no program image field");
    e2 = RS_ERR_MISSING_PROGRAM_IMAGE;
  }
  else if ((fieldstart + hdrstart) % 2) {
    rs_warning(NULL, app, "application header is not a multiple of 2 bytes");
    e2 = RS_ERR_MISALIGNED_PROGRAM_IMAGE;
  }

  md5_buffer((char*) app->data, length, &hash);

  sig = app->data + length;
  if (sig[0] != 0x02 || (sig[1] & 0xf0) != 0x00) {
    rs_error(NULL, app, "application does not have an RSA signature");
    return RS_ERR_MISSING_RSA_SIGNATURE;
  }
  rs_get_field_size(sig, &fieldstart, &fieldsize);

  mpz_init(sigv);
  mpz_init(hashv);

  mpz_import(hashv, 16, -1, 1, 0, 0, hash);
  rs_message(2, NULL, app, "hash = %ZX", hashv);

  mpz_import(sigv, fieldsize, -1, 1, 0, 0, sig + fieldstart);
  rs_message(2, NULL, app, "sig = %ZX", sigv);

  e = rs_validate_rsa(sigv, hashv, key);
  if (e == RS_SIGNATURE_INCORRECT)
    rs_message(0, NULL, app, "application signature incorrect");

  mpz_clear(sigv);
  mpz_clear(hashv);
  return (e ? e : e2);
}

