/*
 * Copyright 1987, 1988 by the Massachusetts Institute of Technology.
 * 
 * For copying and distribution information, please see the file
 * <mit-copyright.h>.
 *
 * Kerberos database manipulation utility. This program allows you to
 * dump a kerberos database to an ascii readable file and load this
 * file into the database. Read locking of the database is done during a
 * dump operation. NO LOCKING is done during a load operation. Loads
 * should happen with other processes shutdown. 
 *
 * Written July 9, 1987 by Jeffrey I. Schiller
 */

#include "adm_locl.h"

RCSID("$Id: kdb_util.c,v 1.42.2.1 2000/10/10 12:59:16 assar Exp $");

static des_cblock master_key, new_master_key;
static des_key_schedule master_key_schedule, new_master_key_schedule;

/* cv_key is a procedure which takes a principle and changes its key, 
   either for a new method of encrypting the keys, or a new master key.
   if cv_key is null no transformation of key is done (other than net byte
   order). */

struct callback_args {
    void (*cv_key)(Principal *);
    FILE *output_file;
};

static void
print_time(FILE *file, time_t timeval)
{
    struct tm *tm;
    tm = gmtime(&timeval);
    fprintf(file, " %04d%02d%02d%02d%02d",
	    tm->tm_year + 1900,
            tm->tm_mon + 1,
            tm->tm_mday,
            tm->tm_hour,
            tm->tm_min);
}

static long
time_explode(char *cp)
{
    char wbuf[5];
    struct tm tp;
    int local;

    memset(&tp, 0, sizeof(tp));	/* clear out the struct */
    
    if (strlen(cp) > 10) {		/* new format */
	strlcpy(wbuf, cp, sizeof(wbuf));
	tp.tm_year = atoi(wbuf) - 1900;
	cp += 4;			/* step over the year */
	local = 0;			/* GMT */
    } else {				/* old format: local time, 
					   year is 2 digits, assuming 19xx */
	wbuf[0] = *cp++;
	wbuf[1] = *cp++;
	wbuf[2] = 0;
	tp.tm_year = atoi(wbuf);
	local = 1;			/* local */
    }

    wbuf[0] = *cp++;
    wbuf[1] = *cp++;
    wbuf[2] = 0;
    tp.tm_mon = atoi(wbuf)-1;

    wbuf[0] = *cp++;
    wbuf[1] = *cp++;
    tp.tm_mday = atoi(wbuf);
    
    wbuf[0] = *cp++;
    wbuf[1] = *cp++;
    tp.tm_hour = atoi(wbuf);
    
    wbuf[0] = *cp++;
    wbuf[1] = *cp++;
    tp.tm_min = atoi(wbuf);

    return(tm2time(tp, local));
}

static int
dump_db_1(void *arg,
	  Principal *principal)	/* replace null strings with "*" */
{
    struct callback_args *a = (struct callback_args *)arg;
    
    if (principal->instance[0] == '\0') {
	principal->instance[0] = '*';
	principal->instance[1] = '\0';
    }
    if (principal->mod_name[0] == '\0') {
	principal->mod_name[0] = '*';
	principal->mod_name[1] = '\0';
    }
    if (principal->mod_instance[0] == '\0') {
	principal->mod_instance[0] = '*';
	principal->mod_instance[1] = '\0';
    }
    if (a->cv_key != NULL) {
	(*a->cv_key) (principal);
    }
    fprintf(a->output_file, "%s %s %d %d %d %d %x %x",
	    principal->name,
	    principal->instance,
	    principal->max_life,
	    principal->kdc_key_ver,
	    principal->key_version,
	    principal->attributes,
	    (int)htonl (principal->key_low),
	    (int)htonl (principal->key_high));
    print_time(a->output_file, principal->exp_date);
    print_time(a->output_file, principal->mod_date);
    fprintf(a->output_file, " %s %s\n",
	    principal->mod_name,
	    principal->mod_instance);
    return 0;
}

static int
dump_db (char *db_file, FILE *output_file, void (*cv_key) (Principal *))
{
    struct callback_args a;

    a.cv_key = cv_key;
    a.output_file = output_file;
    
    kerb_db_iterate (dump_db_1, &a);
    return fflush(output_file);
}

static int
add_file(void *db, FILE *file)
{
    int ret;
    int lineno = 0;
    char line[1024];
    unsigned long key[2]; /* yes, long */
    Principal pr;
    
    char exp_date[64], mod_date[64];
    
    int life, kkvno, kvno;
    
    while(1){
	memset(&pr, 0, sizeof(pr));
	errno = 0;
	if(fgets(line, sizeof(line), file) == NULL){
	    if(errno != 0)
	      err (1, "fgets");
	    break;
	}
	lineno++;
	ret = sscanf(line, "%s %s %d %d %d %hd %lx %lx %s %s %s %s",
		     pr.name, pr.instance,
		     &life, &kkvno, &kvno,
		     &pr.attributes,
		     &key[0], &key[1],
		     exp_date, mod_date,
		     pr.mod_name, pr.mod_instance);
	if(ret != 12){
	    warnx("Line %d malformed (ignored)", lineno);
	    continue;
	}
	pr.key_low = ntohl (key[0]);
	pr.key_high = ntohl (key[1]);
	pr.max_life = life;
	pr.kdc_key_ver = kkvno;
	pr.key_version = kvno;
	pr.exp_date = time_explode(exp_date);
	pr.mod_date = time_explode(mod_date);
	if (pr.instance[0] == '*')
	    pr.instance[0] = 0;
	if (pr.mod_name[0] == '*')
	    pr.mod_name[0] = 0;
	if (pr.mod_instance[0] == '*')
	    pr.mod_instance[0] = 0;
	if (kerb_db_update(db, &pr, 1) != 1) {
	    warn ("store %s.%s aborted",
		  pr.name, pr.instance);
	    return 1;
	}
    }
    return 0;
}

static void
load_db (char *db_file, FILE *input_file)
{
    long *db;
    int code;
    char *temp_db_file;

    asprintf (&temp_db_file, "%s~", db_file);
    if(temp_db_file == NULL)
	errx (1, "out of memory");

    /* Create the database */
    if ((code = kerb_db_create(temp_db_file)) != 0)
	err (1, "creating temp database %s", temp_db_file);
    kerb_db_set_name(temp_db_file);
    db = kerb_db_begin_update();
    if (db == NULL)
	err (1, "opening temp database %s", temp_db_file);
    
    if(add_file(db, input_file))
	errx (1, "Load aborted");

    kerb_db_end_update(db);
    if ((code = kerb_db_rename(temp_db_file, db_file)) != 0)
        warn("database rename failed");
    fclose(input_file);
    free(temp_db_file);
}

static void
merge_db(char *db_file, FILE *input_file)
{
    void *db;
    
    db = kerb_db_begin_update();
    if(db == NULL)
        err (1, "Couldn't open database");
    if(add_file(db, input_file))
        errx (1, "Merge aborted");
    kerb_db_end_update(db);
}

static void
update_ok_file (char *file_name)
{
    /* handle slave locking/failure stuff */
    char *file_ok;
    int fd;

    asprintf (&file_ok, "%s.dump_ok", file_name);
    if (file_ok == NULL)
      errx (1, "out of memory");
    if ((fd = open(file_ok, O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0)
        err (1, "Error creating %s", file_ok);
    free(file_ok);
    close(fd);
    /*
     * Some versions of BSD don't update the mtime in the above open so
     * we call utimes just in case.
     */
    if (utime(file_name, NULL) < 0)
	err (1, "utime %s", file_name);
}

static void
convert_key_new_master (Principal *p)
{
  des_cblock key;

  /* leave null keys alone */
  if ((p->key_low == 0) && (p->key_high == 0)) return;

  /* move current key to des_cblock for encryption, special case master key
     since that's changing */
  if ((strncmp (p->name, KERB_M_NAME, ANAME_SZ) == 0) &&
      (strncmp (p->instance, KERB_M_INST, INST_SZ) == 0)) {
    memcpy (key, new_master_key, sizeof(des_cblock));
    (p->key_version)++;
  } else {
    copy_to_key(&p->key_low, &p->key_high, key);
    kdb_encrypt_key (&key, &key, &master_key,
		     master_key_schedule, DES_DECRYPT);
  }

  kdb_encrypt_key (&key, &key, &new_master_key,
		   new_master_key_schedule, DES_ENCRYPT);

  copy_from_key(key, &(p->key_low), &(p->key_high));
  memset(key, 0, sizeof (key));  /* a little paranoia ... */

  (p->kdc_key_ver)++;
}

static void
clear_secrets (void)
{
  memset(master_key, 0, sizeof (des_cblock));
  memset(master_key_schedule, 0, sizeof (des_key_schedule));
  memset(new_master_key, 0, sizeof (des_cblock));
  memset(new_master_key_schedule, 0, sizeof (des_key_schedule));
}

static void
convert_new_master_key (char *db_file, FILE *out)
{
#ifdef RANDOM_MKEY
  errx (1, "Sorry, this function is not available with "
	"the new master key scheme.");
#else
  printf ("\n\nEnter the CURRENT master key.");
  if (kdb_get_master_key (KDB_GET_PROMPT, &master_key,
			  master_key_schedule) != 0) {
    clear_secrets ();
    errx (1, "Couldn't get master key.");
  }

  if (kdb_verify_master_key (&master_key, master_key_schedule, stderr) < 0) {
    clear_secrets ();
    exit (1);
  }

  printf ("\n\nNow enter the NEW master key.  Do not forget it!!");
  if (kdb_get_master_key (KDB_GET_TWICE, &new_master_key,
			  new_master_key_schedule) != 0) {
    clear_secrets ();
    errx (1, "Couldn't get new master key.");
  }

  dump_db (db_file, out, convert_key_new_master);
  {
    char *fname;

    asprintf(&fname, "%s.new", MKEYFILE);
    if(fname == NULL) {
      clear_secrets();
      errx(1, "malloc: failed");
    }
    kdb_kstash(&new_master_key, fname);
    free(fname);
  }
#endif /* RANDOM_MKEY */
}

static void
convert_key_old_db (Principal *p)
{
  des_cblock key;

 /* leave null keys alone */
  if ((p->key_low == 0) && (p->key_high == 0)) return;

  copy_to_key(&p->key_low, &p->key_high, key);

#ifndef NOENCRYPTION
  des_pcbc_encrypt((des_cblock *)key,(des_cblock *)key,
	(long)sizeof(des_cblock),master_key_schedule,
	(des_cblock *)master_key_schedule, DES_DECRYPT);
#endif

  /* make new key, new style */
  kdb_encrypt_key (&key, &key, &master_key, master_key_schedule, DES_ENCRYPT);

  copy_from_key(key, &(p->key_low), &(p->key_high));
  memset(key, 0, sizeof (key));  /* a little paranoia ... */
}

static void
convert_old_format_db (char *db_file, FILE *out)
{
  des_cblock key_from_db;
  Principal principal_data[1];
  int n, more;

  if (kdb_get_master_key (KDB_GET_PROMPT, &master_key,
			  master_key_schedule) != 0L) {
    clear_secrets();
    errx (1, "Couldn't get master key.");
  }

  /* can't call kdb_verify_master_key because this is an old style db */
  /* lookup the master key version */
  n = kerb_get_principal(KERB_M_NAME, KERB_M_INST, principal_data,
			 1 /* only one please */, &more);
  if ((n != 1) || more)
    errx (1, "verify_master_key: Kerberos error on master key lookup, %d found.\n", n);

  /* set up the master key */
  fprintf(stderr, "Current Kerberos master key version is %d.\n",
	  principal_data[0].kdc_key_ver);

  /*
   * now use the master key to decrypt (old style) the key in the db, had better
   * be the same! 
   */
  copy_to_key(&principal_data[0].key_low,
	      &principal_data[0].key_high,
	      key_from_db);
#ifndef NOENCRYPTION
  des_pcbc_encrypt(&key_from_db,&key_from_db,(long)sizeof(key_from_db),
	master_key_schedule,(des_cblock *)master_key_schedule, DES_DECRYPT);
#endif
  /* the decrypted database key had better equal the master key */

  n = memcmp(master_key, key_from_db, sizeof(master_key));
  memset(key_from_db, 0, sizeof(key_from_db));

  if (n) {
    fprintf(stderr, "\n\07\07verify_master_key: Invalid master key, ");
    fprintf(stderr, "does not match database.\n");
    exit (1);
  }
    
  fprintf(stderr, "Master key verified.\n");

  dump_db (db_file, out, convert_key_old_db);
}

int
main(int argc, char **argv)
{
    int ret;
    FILE   *file;
    enum {
	OP_LOAD,
	OP_MERGE,
	OP_DUMP,
	OP_SLAVE_DUMP,
	OP_NEW_MASTER,
	OP_CONVERT_OLD_DB
    }       op;
    char *file_name;
    char *db_name;

    set_progname (argv[0]);
    
    if (argc != 3 && argc != 4) {
	fprintf(stderr, "Usage: %s operation file [database name].\n",
		argv[0]);
	fprintf(stderr, "Operation is one of: "
		"load, merge, dump, slave_dump, new_master_key, "
		"convert_old_db\n");
	fprintf(stderr, "use file `-' for stdout\n");
	exit(1);
    }
    if (argc == 3)
	db_name = DBM_FILE;
    else
	db_name = argv[3];
    
    ret = kerb_db_set_name (db_name);
    
    /* this makes starting slave servers ~14.3 times easier */
    if(ret && strcmp(argv[1], "load") == 0)
       ret = kerb_db_create (db_name);

    if(ret)
      err (1, "Can't open database");

    if (!strcmp(argv[1], "load"))
	op = OP_LOAD;
    else if (!strcmp(argv[1], "merge"))
	op = OP_MERGE;
    else if (!strcmp(argv[1], "dump"))
	op = OP_DUMP;
    else if (!strcmp(argv[1], "slave_dump"))
        op = OP_SLAVE_DUMP;
    else if (!strcmp(argv[1], "new_master_key"))
        op = OP_NEW_MASTER;
    else if (!strcmp(argv[1], "convert_old_db"))
        op = OP_CONVERT_OLD_DB;
    else {
        warnx ("%s is an invalid operation.", argv[1]);
	warnx ("Valid operations are \"load\", \"merge\", "
	       "\"dump\", \"slave_dump\", \"new_master_key\", "
	       "and \"convert_old_db\"");
	return 1;
    }

    file_name = argv[2];
    if (strcmp (file_name, "-") == 0
	&& op != OP_LOAD
	&& op != OP_MERGE)
	file = stdout;
    else {
	char *mode;

	if (op == OP_LOAD || op == OP_MERGE)
	    mode = "r";
	else
	    mode = "w";

	file = fopen (file_name, mode);
    }
    if (file == NULL)
        err (1, "open %s", argv[2]);

    switch (op) {
    case OP_DUMP:
      if ((dump_db(db_name, file, (void (*)(Principal *)) 0) == EOF)
	  || (fflush(file) != 0)
	  || (fsync(fileno(file)) != 0)
	  || (fclose(file) == EOF))
	err(1, "%s", file_name);
      break;
    case OP_SLAVE_DUMP:
      if ((dump_db(db_name, file, (void (*)(Principal *)) 0) == EOF)
	  || (fflush(file) != 0)
	  || (fsync(fileno(file)) != 0)
	  || (fclose(file) == EOF))
	err(1, "%s", file_name);
      update_ok_file(file_name);
      break;
    case OP_LOAD:
      load_db (db_name, file);
      break;
    case OP_MERGE:
      merge_db (db_name, file);
      break;
    case OP_NEW_MASTER:
      convert_new_master_key (db_name, file);
      printf("Don't forget to do a `kdb_util load %s' to reload the database!\n", file_name);
      break;
    case OP_CONVERT_OLD_DB:
      convert_old_format_db (db_name, file);
      printf("Don't forget to do a `kdb_util load %s' to reload the database!\n", file_name);      
      break;
    }
    return 0;
}
