/*
 * Copyright (c) 2003-2004 Apple Computer, Inc. All Rights Reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 * 
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 * 
 * @APPLE_LICENSE_HEADER_END@
 *
 * security.c
 */

#include "security.h"

#include "leaks.h"
#include "readline.h"

#include "cmsutil.h"
#include "db_commands.h"
#include "keychain_add.h"
#include "keychain_create.h"
#include "keychain_delete.h"
#include "keychain_list.h"
#include "keychain_lock.h"
#include "keychain_set_settings.h"
#include "keychain_show_info.h"
#include "keychain_unlock.h"
#include "keychain_recode.h"
#include "key_create.h"
#include "keychain_find.h"
#include "keychain_import.h"
#include "keychain_export.h"
#include "mds_install.h"

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <CoreFoundation/CFRunLoop.h>
#include <Security/SecBasePriv.h>
#include <security_asn1/secerr.h>

/* Maximum length of an input line in interactive mode. */
#define MAX_LINE_LEN 4096
/* Maximum number of arguments on an input line in interactive mode. */
#define MAX_ARGS 32

/* Entry in commands array for a command. */
typedef struct command
{
	const char *c_name;    /* name of the command. */
	command_func c_func;   /* function to execute the command. */
	const char *c_usage;   /* usage sting for command. */
	const char *c_help;    /* help string for (or description of) command. */
} command;

/* The default prompt. */
const char *prompt_string = "security> ";

/* The name of this program. */
const char *prog_name;


/* Forward declarations of static functions. */
static int help(int argc, char * const *argv);

/*
 * The command array itself.
 * Add commands here at will.
 * Matching is done on a prefix basis.  The first command in the array
 * gets matched first.
 */
const command commands[] =
{
	{ "help", help,
	  "[command ...]",
	  "Show all commands. Or show usage for a command." },
    
	{ "list-keychains", keychain_list,
	  "[-d user|system|common|alternate] [-s [keychain...]]\n"
	  "    -d    Use the specified domain.\n"
	  "    -s    Set the searchlist to the specified keychains.\n"
	  "With no parameters display the searchlist.",
	  "Display or manipulate the keychain search list." },

	{ "default-keychain", keychain_default,
	  "[-d user|system|common|alternate] [-s [keychain]]\n"
	  "    -d    Use the specified domain.\n"
	  "    -s    Set the default keychain to the specified keychain.\n"
	  "With no parameters display the default keychain.",
	  "Display or set the default keychain." },

	{ "login-keychain", keychain_login,
	  "[-d user|system|common|alternate] [-s [keychain]]\n"
	  "    -d    Use the specified domain.\n"
	  "    -s    Set the login keychain to the specified keychain.\n"
	  "With no parameters display the login keychain.",
	  "Display or set the login keychain." },

	{ "create-keychain", keychain_create,
	  "[-P] [-p password] [keychains...]\n"
	  "    -p    Use \"password\" as the password for the keychains being created.\n"
	  "    -P    Prompt the user for a password using the SecurityAgent.",
	  "Create keychains and add them to the search list." },

	{ "delete-keychain", keychain_delete,
	  "[keychains...]",
	  "Delete keychains and remove them from the search list." },

    { "lock-keychain", keychain_lock,
      "[-a | keychain]\n"
      "     -a Lock all keychains.",
      "Lock the specified keychain."},

    { "unlock-keychain", keychain_unlock,
      "[-u] [-p password] [keychain]\n"
      "     -p Use \"password\" as the password to unlock the keychain.\n"
      "     -u Do not use the password.",
      "Unlock the specified keychain."},

    { "set-keychain-settings", keychain_set_settings,
      "[-lu] [-t locktimeout] [keychain]\n"
      "     -l  Lock keychain when the system sleeps.\n"
      "     -u  Lock keychain after certain period of time.\n"
      "     -t  Timeout in seconds before the keychain locks.\n",
      "Set settings for a keychain."},

    { "show-keychain-info", keychain_show_info,
	  "[keychain]",
	  "Show the settings for keychain." },

    { "dump-keychain", keychain_dump,
	  "[-adir] [keychain...]\n"
      "     -a  Dump acl of items.\n"
      "     -d  Dump data of items.\n"
      "     -i  Interactive acl editing mode.\n"
      "     -r  Dump the raw (encrypted) data of items.",
	  "Dump the contents of one or more keychains." },

#ifndef NDEBUG
    { "recode-keychain", keychain_recode,
      "keychain_to_recode keychain_to_get_secrets_from",
      "Recode a keychain to use the secrets from another one."},
#endif

    { "create-keypair", key_create_pair,
	  "[-a alg] [-s size] [-f date] [-t date] [-d days] [-k keychain] [-A|-T app1] description\n"
	  "    -a  Use alg as the algorithm, can be rsa, dh, dsa or fee (default rsa)\n"
	  "    -s  Specify the keysize in bits (default 512)\n"
	  "    -f  Make a key valid from the specified date\n"
	  "    -t  Make a key valid to the specified date\n"
	  "    -d  Make a key valid for the number of days specified from today\n"
	  "    -k  Use the specified keychain rather than the default\n"
	  "    -A  Allow any application to access without warning\n"
	  "    -T  Allow the application specified to access without warning (multiple -T options are allowed).\n"
	  "If no options are provided ask the user interactively",
	  "Create an assymetric keypair." },

    { "create-csr", csr_create,
	  "[-a alg] [-s size] [-f date] [-t date] [-d days] [-k keychain] [-A|-T app1] description\n"
	  "    -a  Use alg as the algorithm, can be rsa, dh, dsa or fee (default rsa)\n"
	  "    -s  Specify the keysize in bits (default 512)\n"
	  "    -f  Make a key valid from the specified date\n"
	  "    -t  Make a key valid to the specified date\n"
	  "    -d  Make a key valid for the number of days specified from today\n"
	  "    -k  Use the specified keychain rather than the default\n"
	  "    -A  Allow any application to access without warning\n"
	  "    -T  Allow the application specified to access without warning (multiple -T options are allowed).\n"
	  "If no options are provided ask the user interactively",
	  "Create an assymetric keypair." },

    { "add-internet-password", keychain_add_internet_password,
	  "[-a accountName] [-d securityDomain] [-p path] [-P port] [-r protocol] [-s serverName] [-t authenticationType] [-w passwordData] [keychain]\n"
	  "    -a Use \"accountName\".\n"
	  "    -d Use \"securityDomain\".\n"
	  "    -p Use \"path\".\n"
	  "    -P Use \"port\".\n"
	  "    -r Use \"protocol\".\n"
	  "    -s Use \"serverName\".\n"
	  "    -t Use \"authenticationType\".\n"
      "    -w Use passwordData.\n"
	  "If no keychains is specified the password is added to the default keychain.",
      "Add an internet password item."},

	{ "add-generic-password", keychain_add_generic_password,
	  "[-a accountName] [-s serviceName] [-p passwordData] [keychain]\n"
	  "    -a Use \"accountName\".\n"
	  "    -s Use \"serverName\".\n"  
      "    -p Use passwordData.\n"
	  "If no keychains is specified the password is added to the default keychain.",
      "Add a generic password item."},
      
	{ "add-certificates", keychain_add_certificates,
	  "[-k keychain] file...\n"
	  "If no keychains is specified the certificates are added to the default keychain.",
      "Add certificates to a keychain."},

	{ "find-internet-password", keychain_find_internet_password,
	  "[-a accountName] [-d securityDomain] [-g] [-p path] [-P port] [-r protocol] [-s serverName] [-t authenticationType] [keychain...]\n"
	  "    -a Match on \"accountName\" when searching.\n"
	  "    -d Match on \"securityDomain\" when searching.\n"
	  "    -g Display the password for the item found.\n"
	  "    -p Match on \"path\" when searching.\n"
	  "    -P Match on \"port\" when searching.\n"
	  "    -r Match on \"protocol\" when searching.\n"
	  "    -s Match on \"serverName\" when searching.\n"
	  "    -t Match on \"authenticationType\" when searching.\n"
	  "If no keychains are specified the default search list is used.",
      "Find an internet password item."},

	{ "find-generic-password", keychain_find_generic_password,
	  "[-a accountName] [-s serviceName] [keychain...]\n"
	  "    -a Match on \"accountName\" when searching.\n"
      "    -g Display the password for the item found.\n"
	  "    -s Match on \"serviceName\" when searching.\n"
	  	  "If no keychains are specified the default search list is used.",
      "Find a generic password item."},

	{ "find-certificate", keychain_find_certificate,
	  "[-a] [-e emailAddress] [-m] [-p] [keychain...]\n"
	  "    -a Find all matching certificates, not just the first one.\n"
	  "    -e Match on \"emailAddress\" when searching.\n"
	  "    -m Show the \"emailAddresses\" in the certificate.\n"
	  "    -p Output certificate in pem form.\n"
	  	  "If no keychains are specified the default search list is used.",
      "Find a certificate item."},

    { "create-db", db_create,
	  "[-ao0] [-g dl|cspdl] [-m mode] [name]\n"
	  "    -a  Turn off autocommit\n"
	  "    -g  Attach to \"guid\" rather than the AppleFileDL\n"
	  "    -m  Set the inital mode of the created db to \"mode\"\n"
	  "    -o  Force using openparams argument\n"
	  "    -0  Force using version 0 openparams\n"
	  "If no name is provided ask the user interactively",
	  "Create an db using the DL." },

	{ "export" , keychain_export,
	  "[-k keychain] [-t item_type] [-f item_format] [-w] -p [-P passphrase] [-o outfile]\n"
	  "    -k  keychain to export items from\n"
	  "    -t  item_type = certs|allKeys|pubKeys|privKeys|identities|all  default=all\n"
	  "    -f  format = openssl|openssh|bsafe|pkcs7|pkcs8|pkcs12|pemseq|x509\n"
	  "        ...default format is pemseq for aggregate, openssl for single\n"
	  "    -w  Private keys are wrapped\n"
	  "    -p  PEM encode\n"
	  "    -P  Specify wrapping passphrase immediately (default is secure passphrase via GUI)\n"
	  "    -o  Specify output file; default is stdout",
	  "Export an item into a keychain." },

	{ "import" , keychain_import,
	  "inputfile [-k keychain] [-t item_type] [-f item_format] [-w] [-P passphrase]\n"
	  "    -k  Target keychain to import into\n"
	  "    -t  item = pub|priv|session|cert|agg\n"
	  "    -f  Format = openssl|openssh|bsafe|raw|pkcs7|pkcs8|pkcs12|netscape|pemseq\n"
	  "    -w  Private keys are wrapped\n"
	  "    -P  Specify wrapping passphrase immediately (default is secure passphrase via GUI)\n",
	  "Import an item into a keychain." },

	{ "cms", cms_util,
        "[-D|-S|-E] [<options>] [-d dbdir] [-u certusage]\n"
        "  -D           decode a CMS message\n"
        "  -c content   use this detached content\n"
        "  -n           suppress output of content\n"
        "  -h num       generate email headers with info about CMS message\n"
        "  -S           create a CMS signed message\n"
        "  -G           include a signing time attribute\n"
        "  -H hash      use hash (default:SHA1)\n"
        "  -N nick      use certificate named \"nick\" for signing\n"
        "  -P           include a SMIMECapabilities attribute\n"
        "  -T           do not include content in CMS message\n"
        "  -Y nick      include a EncryptionKeyPreference attribute with cert\n"
        "                 (use \"NONE\" to omit)\n"
        "  -E           create a CMS enveloped message (NYI)\n"
        "  -r id,...    create envelope for these recipients,\n"
        "               where id can be a certificate nickname or email address\n"
        "  -k keychain  keychain to use\n"
        "  -i infile    use infile as source of data (default: stdin)\n"
        "  -o outfile   use outfile as destination of data (default: stdout)\n"
        "  -p password  use password as key db password (default: prompt)\n"
        "  -s           pass in data single byte at a time to cms layer\n"
        "  -u certusage set type of certificate usage (default: certUsageEmailSigner)\n"
        "  -v           print debugging information\n\n"
        "Cert usage codes:\n"
        "                           0 - certUsageSSLClient\n"
        "                           1 - certUsageSSLServer\n"
        "                           2 - certUsageSSLServerWithStepUp\n"
        "                           3 - certUsageSSLCA\n"
        "                           4 - certUsageEmailSigner\n"
        "                           5 - certUsageEmailRecipient\n"
        "                           6 - certUsageObjectSigner\n"
        "                           7 - certUsageUserCertImport\n"
        "                           8 - certUsageVerifyCA\n"
        "                           9 - certUsageProtectedObjectSigner\n"
        "                          10 - certUsageStatusResponder\n"
        "                          11 - certUsageAnyCA",
        "Manipulate cms messages."},
    
	{ "install-mds" , mds_install,
	  "",		/* no options */
	  "Install (or re-install) the MDS database." },

	{ "leaks", leaks,
	  "[-cycles] [-nocontext] [-nostacks] [-exclude symbol]\n"
	  "    -cycles       Use a stricter algorithm (Man leaks for details).\n"
	  "    -nocontext    Withhold the hex dumps of the leaked memory.\n"
	  "    -nostacks     Don't show stack traces fo leaked memory.\n"
	  "    -exclude      Ignore leaks called from \"symbol\".\n"
	  "(Set the environment variable MallocStackLogging to get symbolic traces.)",
	  "Run /usr/bin/leaks on this proccess." },

	{}
};

/* Global variables. */
int do_quiet = 0;
int do_verbose = 0;

/* Return 1 if name matches command. */
static int
match_command(const char *command, const char *name)
{
	return !strncmp(command, name, strlen(name));
}

/* The help command. */
static int
help(int argc, char * const *argv)
{
	const command *c;

	if (argc > 1)
	{
		char * const *arg;
		for (arg = argv + 1; *arg; ++arg)
		{
			int found = 0;

			for (c = commands; c->c_name; ++c)
			{
				if (match_command(c->c_name, *arg))
				{
					found = 1;
					break;
				}
			}

			if (found)
				printf("Usage: %s %s\n", c->c_name, c->c_usage);
			else
			{
				sec_error("%s: no such command: %s", argv[0], *arg);
				return 1;
			}
		}
	}
	else
	{
		for (c = commands; c->c_name; ++c)
			printf("    %-17s %s\n", c->c_name, c->c_help);
	}

	return 0;
}

/* States for split_line parser. */
typedef enum
{
	SKIP_WS,
	READ_ARG,
	READ_ARG_ESCAPED,
	QUOTED_ARG,
	QUOTED_ARG_ESCAPED
} parse_state;

/* Split a line into multiple arguments and return them in *pargc and *pargv. */
static void
split_line(char *line, int *pargc, char * const **pargv)
{
	static char *argvec[MAX_ARGS + 1];
	int argc = 0;
	char *ptr = line;
	char *dst = line;
	parse_state state = SKIP_WS;
	int quote_ch = 0;

	for (ptr = line; *ptr; ++ptr)
	{
		if (state == SKIP_WS)
		{
			if (isspace(*ptr))
				continue;

			if (*ptr == '"' || *ptr == '\'')
			{
				quote_ch = *ptr;
				state = QUOTED_ARG;
				argvec[argc] = dst;
				continue; /* Skip the quote. */
			}
			else
			{
				state = READ_ARG;
				argvec[argc] = dst;
			}
		}

		if (state == READ_ARG)
		{
			if (*ptr == '\\')
			{
				state = READ_ARG_ESCAPED;
				continue;
			}
			else if (isspace(*ptr))
			{
				/* 0 terminate each arg. */
				*dst++ = '\0';
				argc++;
				state = SKIP_WS;
				if (argc >= MAX_ARGS)
					break;
			}
			else
				*dst++ = *ptr;
		}

		if (state == QUOTED_ARG)
		{
			if (*ptr == '\\')
			{
				state = QUOTED_ARG_ESCAPED;
				continue;
			}
			if (*ptr == quote_ch)
			{
				/* 0 terminate each arg. */
				*dst++ = '\0';
				argc++;
				state = SKIP_WS;
				if (argc >= MAX_ARGS)
					break;
			}
			else
				*dst++ = *ptr;
		}

		if (state == READ_ARG_ESCAPED)
		{
			*dst++ = *ptr;
			state = READ_ARG;
		}

		if (state == QUOTED_ARG_ESCAPED)
		{
			*dst++ = *ptr;
			state = QUOTED_ARG;
		}
	}

	if (state != SKIP_WS)
	{
		/* Terminate last arg. */
		*dst++ = '\0';
		argc++;
	}

	/* Teminate arg vector. */
	argvec[argc] = NULL;

	*pargv = argvec;
	*pargc = argc;
}

/* Print a (hopefully) useful usage message. */
static int
usage(void)
{
	printf(
		"Usage: %s [-h] [-i] [-l] [-p prompt] [-q] [-v] [command] [opt ...]\n"
		"    -i    Run in interactive mode.\n"
		"    -l    Run /usr/bin/leaks -nocontext before exiting.\n"
		"    -p    Set the prompt to \"prompt\" (implies -i).\n"
		"    -q    Be less verbose.\n"
		"    -v    Be more verbose about what's going on.\n"
		"%s commands are:\n", prog_name, prog_name);
	help(0, NULL);
	return 2;
}

/* Execute a single command. */ 
static int
execute_command(int argc, char * const *argv)
{
	const command *c;
	int found = 0;

	/* Nothing to do. */
	if (argc == 0)
		return 0;

	for (c = commands; c->c_name; ++c)
	{
		if (match_command(c->c_name, argv[0]))
		{
			found = 1;
			break;
		}
	}

	if (found)
	{
		int result;

		/* Reset getopt for command proc. */
		optind = 1;
		optreset = 1;

		if (do_verbose)
		{
			int ix;

			fprintf(stderr, "%s", c->c_name);
			for (ix = 1; ix < argc; ++ix)
				fprintf(stderr, " \"%s\"", argv[ix]);
			fprintf(stderr, "\n");
		}

		result = c->c_func(argc, argv);
		if (result == 2)
			fprintf(stderr, "Usage: %s %s\n        %s\n", c->c_name, c->c_usage, c->c_help);

		return result;
	}
	else
	{
		sec_error("unknown command \"%s\"", argv[0]);
		return 1;
	}
}

static void
receive_notifications(void)
{
	/* Run the CFRunloop to get any pending notifications. */
	while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, TRUE) == kCFRunLoopRunHandledSource);
}


const char *
sec_errstr(int err)
{
    const char *errString;
    if (IS_SEC_ERROR(err))
        errString = SECErrorString(err);
    else
        errString = cssmErrorString(err);
    return errString;
}

void
sec_error(const char *msg, ...)
{
    va_list args;

    fprintf(stderr, "%s: ", prog_name);

    va_start(args, msg);
    vfprintf(stderr, msg, args);
    va_end(args);

    fprintf(stderr, "\n");
}

void
sec_perror(const char *msg, int err)
{
    sec_error("%s: %s", msg, sec_errstr(err));
}

int
main(int argc, char * const *argv)
{
	int result = 0;
	int do_help = 0;
	int do_interactive = 0;
	int do_leaks = 0;
	int ch;

	/* Remember my name. */
	prog_name = strrchr(argv[0], '/');
	prog_name = prog_name ? prog_name + 1 : argv[0];

	/* Do getopt stuff for global options. */
	optind = 1;
	optreset = 1;
	while ((ch = getopt(argc, argv, "hilp:qv")) != -1)
	{
		switch  (ch)
		{
		case 'h':
			do_help = 1;
			break;
		case 'i':
			do_interactive = 1;
			break;
		case 'l':
			do_leaks = 1;
			break;
		case 'p':
			do_interactive = 1;
			prompt_string = optarg;
			break;
		case 'q':
			do_quiet = 1;
			break;
		case 'v':
			do_verbose = 1;
			break;
		case '?':
		default:
			return usage();
		}
	}

	argc -= optind;
	argv += optind;

	if (do_help)
	{
		/* Munge argc/argv so that argv[0] is something. */
		return help(argc + 1, argv - 1);
	}
	else if (argc > 0)
	{
		receive_notifications();
		result = execute_command(argc, argv);
		receive_notifications();
	}
	else if (do_interactive)
	{
		/* In interactive mode we just read commands and run them until readline returns NULL. */

        /* Only show prompt string if stdin is a tty. */
        int show_prompt = isatty(0);

		for (;;)
		{
			static char buffer[MAX_LINE_LEN];
			char * const *av, *input;
			int ac;

            if (show_prompt)
                fprintf(stderr, "%s", prompt_string);

			input = readline(buffer, MAX_LINE_LEN);
			if (!input)
				break;

			split_line(input, &ac, &av);
			receive_notifications();
			result = execute_command(ac, av);
			receive_notifications();
			if (result == -1)
			{
				result = 0;
				break;
			}

			if (result && ! do_quiet)
			{
				fprintf(stderr, "%s: returned %d\n", av[0], result);
			}
		}
	}
	else
		result = usage();

	if (do_leaks)
	{
		char *const argvec[3] = { "leaks", "-nocontext", NULL };
		leaks(2, argvec);
	}

	return result;
}
