/* (C) folkert van heusden
 * released in the public domain
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <ctype.h>
#include <gpgme.h>

#define MODE_RANDOM		1
#define MODE_INCREMENTAL	2
#define MODE_FILE		3

#define MAX_PP_LEN		128

unsigned char passphrase[MAX_PP_LEN + 2] = { 0 };
unsigned char passphrase_template[MAX_PP_LEN + 2] = { 0 };
int min_length = 1;
int max_length = MAX_PP_LEN;

char *pp_file_in = NULL; 
FILE *pp_file_in_fh = NULL;
long pp_file_in_offset = 0;	/* this kludge is to make sure we have the file-
				 * offset *before* the current passphrase as we
				 * might get the signal while the current one
				 * still is being processed
				 */
char *pp_file_out = NULL;

unsigned char charset[256] = { 0 };
int charset_n = 1;

int mode = MODE_INCREMENTAL;
/* int mode = MODE_RANDOM; */

char verbose = 0;

long long int n_done = 0;

void error_exit(char *str, gpgme_error_t err)
{
	fprintf(stderr, "%s: %s\n", str, gpgme_strerror(err));

	exit(1);
}

char valtohexdigit(int n)
{
	if (n > 9)
		return 'A' + (n - 10);

	return '0' + n;
}

int hexdigittoval(char hex)
{
	hex = toupper(hex);

	if (hex >= 'A')
		return hex - 'A' + 10;

	return hex - '0';
}

int cvt_from_hex_string(char *in, char *out)
{
	int loop, len = strlen(in), index = 0;

	for(loop=0; loop<len; loop+=2)
	{
		out[index++] = (hexdigittoval(in[loop]) << 4) + hexdigittoval(in[loop + 1]);
	}

	return index;
}

void sighandler(int sig)
{
	FILE *fh = fopen("nasty.state", "w");
	if (fh)
	{
		int loop, len;

		fprintf(fh, "# Do not edit this file (unless you're really sure what you're doing)!\n\n");

		fprintf(fh, "mode=%d\n", mode);

		len = strlen((char *)passphrase_template);
		fprintf(fh, "cur=");
		for(loop=0; loop<len; loop++)
			fprintf(fh, "%c%c", valtohexdigit(passphrase_template[loop] >> 4), valtohexdigit(passphrase_template[loop] & 15));
		fprintf(fh, "\n");

		fprintf(fh, "charset=");
		for(loop=0; loop<charset_n; loop++)
			fprintf(fh, "%c%c", valtohexdigit(charset[loop] >> 4), valtohexdigit(charset[loop] & 15));
		fprintf(fh, "\n");

		fprintf(fh, "min_length=%d\n", min_length);
		fprintf(fh, "max_length=%d\n", max_length);

		fprintf(fh, "n_done=%lld\n", n_done);

		if (mode == MODE_FILE)
		{
			fprintf(fh, "file=%s\n", pp_file_in);
			fprintf(fh, "file_offset=%ld\n", pp_file_in_offset);
		}

		fclose(fh);

		printf("\nState saved to 'nasty.state'\n");

		exit(1);
	}
}

void not_found(void)
{
	if (pp_file_out)
	{
		FILE *fh = fopen(pp_file_out, "w");
		if (fh)
		{
			fprintf(fh, "passphrase_not_found\n");
			fclose(fh);
		}
		else
			fprintf(stderr, "Failed to create file '%s': %s\n", pp_file_out, strerror(errno));
	}
	else
	{
		printf("Passphrase not found.\n");
	}

	exit(1);
}

/* Request a passphrase from the user.  */
gpgme_error_t passphrase_cb(void *hook, const char *uid_hint, const char *passphrase_info, int prev_was_bad, int fd)
{
	int loop, len = 0;

	if (mode == MODE_INCREMENTAL)
	{
		int index = 0;

		for(;;)
		{
			if (passphrase_template[index] < (charset_n - 1))
			{
				passphrase_template[index]++;
				break;
			}
			else
			{
				passphrase_template[index] = 1;
				index++;

				if (index == max_length)
					not_found();
			}
		}

		len = strlen((char *)passphrase_template);
	}
	else if (mode == MODE_RANDOM)
	{
		do
		{
			len = (rand() % (max_length - min_length)) + min_length;
		}
		while(len < min_length || len > max_length);

		for(loop=0; loop<len; loop++)
		{
			passphrase_template[loop] = (rand() % (charset_n - 1)) + 1;
		}

		passphrase_template[len] = 0x00;
	}
	else if (mode == MODE_FILE)
	{
		char *p = NULL;

		pp_file_in_offset = ftell(pp_file_in_fh);

		fgets((char *)passphrase, sizeof passphrase, pp_file_in_fh);

		p = strchr((char *)passphrase, '\r');
		if (p)
			*p = 0x00;
		p = strchr((char *)passphrase, '\n');
		if (p)
			*p = 0x00;

		if (passphrase[0] == 0x00) {
			not_found();
			exit(1);
		}
	}

	if (mode != MODE_FILE)
	{
		for(loop=0; loop<len; loop++)
			passphrase[loop] = charset[passphrase_template[loop]];

		passphrase[loop] = 0x00;
	}

	n_done++;

	if (verbose)
		printf("trying [%s]\n", passphrase);

	strcat((char *)passphrase, "\n");

	if (write(fd, (char *)passphrase, strlen((char *)passphrase)) == -1)
		error_exit("Failed sending passphrase to gpg(me)", errno);

	return 0;
}

void open_pp_file_in(void)
{
	pp_file_in_fh = fopen(pp_file_in, "r");
	if (!pp_file_in_fh)
	{
		printf("Failed to open passphrase-file %s: %s\n", pp_file_in, strerror(errno));
		exit(1);
	}
}

int load_state(void)
{
	int ok = 0;
	FILE *fh = fopen("nasty.state", "r");
	if (fh)
	{
		char buffer[4096], *dummy;

		while(!feof(fh))
		{
			memset(buffer, 0x00, 4096);

			if (!fgets(buffer, 4096, fh))
				break;

			dummy = strchr(buffer, '\n');
			if (dummy) *dummy = 0x00;

			if (buffer[0] == '#' || buffer[0] == ';' || strlen(buffer) == 0)
				continue;

			dummy = strchr(buffer, '=');
			if (!dummy) goto state_file_corrupted;

			*dummy = 0x00;
			dummy++;

			if (strcmp(buffer, "cur") == 0)
			{
				int len = cvt_from_hex_string(dummy, (char *)passphrase_template);

				passphrase_template[len] = 0x00;
				passphrase_template[0]--; /* make sure that it is fully checked */
			}
			else if (strcmp(buffer, "charset") == 0)
			{
				charset_n = cvt_from_hex_string(dummy, (char *)charset);
			}
			else if (strcmp(buffer, "min_length") == 0)
			{
				min_length = atoi(dummy);
			}
			else if (strcmp(buffer, "max_length") == 0)
			{
				max_length = atoi(dummy);
			}
			else if (strcmp(buffer, "n_done") == 0)
			{
				n_done = atoll(dummy);
			}
			else if (strcmp(buffer, "mode") == 0)
			{
				mode = atoi(dummy);
			}
			else if (strcmp(buffer, "file") == 0)
			{
				pp_file_in = strdup(dummy);
				if (!pp_file_in)
				{
					printf("Memory allocation failure.\n");
					exit(1);
				}

				open_pp_file_in();
			}
			else if (strcmp(buffer, "file_offset") == 0)
			{
				if (!pp_file_in_fh) goto state_file_corrupted;

				if (fseek(pp_file_in_fh, atol(dummy), SEEK_SET) == -1)
				{
					printf("Failed to seek in passphrase-file: %s\n", strerror(errno));
					exit(1);
				}
			}
			else
			{
state_file_corrupted:
				fprintf(stderr, "nasty.state is corrupted!\n");
				fprintf(stderr, "(%s=%s is not understood)\n", buffer, dummy);
				exit(1);
			}
		}
		fclose(fh);
	}
	else
	{
		ok = -1;
	}

	if (ok == 0)
		printf("Using state from previous run\n");

	return ok;
}

void add_charset_range(int start, int end)
{
	int loop;

	for(loop=start; loop<=end; loop++)
	{
		charset[charset_n++] = (unsigned char)loop;
	}
}

void usage(void)
{
	fprintf(stderr, "-a x	set minimum length of passphrase\n");
	fprintf(stderr, "-b x	set maximum length\n");
	fprintf(stderr, "-m x	set guessing mode:\n");
	fprintf(stderr, "	incremental: try them all\n");
	fprintf(stderr, "	random: try at random\n");
	fprintf(stderr, "	file: read phrases from file (use -i)\n");
	fprintf(stderr, "-i x	file to read the passphrases from\n");
	fprintf(stderr, "-f x	file to write the found passphrase to\n");
	fprintf(stderr, "-c x... charset, one or more from the following:\n");
	fprintf(stderr, "	a: a-z\n");
	fprintf(stderr, "	A: A-Z\n");
	fprintf(stderr, "	0: 0-9\n");
	fprintf(stderr, "	.: all ascii values (32...126)\n");
	fprintf(stderr, "	+: 32...255 (default(!))\n");
	fprintf(stderr, "-k x	filter string to select a key\n");
	fprintf(stderr, "-v 	enable verbose mode\n");
}

void filter_keys_and_select_first(gpgme_ctx_t context, const char *key_filter_string, gpgme_key_t *selected_key)
{
	gpgme_error_t err;
	gpgme_key_t key;

	err = gpgme_op_keylist_start(context, key_filter_string, 1);
	if (err != GPG_ERR_NO_ERROR) error_exit("gpgme_op_keylist_start failed", err);
	err = gpgme_op_keylist_next(context, &key);
	if (err != GPG_ERR_NO_ERROR) error_exit("gpgme_op_keylist_next failed", err);
	err = gpgme_op_keylist_end(context);
	if (err != GPG_ERR_NO_ERROR) error_exit("gpgme_op_keylist_end failed", err);
	err = gpgme_signers_add(context, key);
	if (err != GPG_ERR_NO_ERROR) error_exit("gpgme_signers_add failed", err);
	gpgme_key_unref(key);

	printf ("Using %s:", key->subkeys->keyid);
	if (key->uids && key->uids->name) printf(" %s", key->uids->name);
	if (key->uids && key->uids->email) printf(" <%s>", key->uids->email);
	putchar ('\n');
	*selected_key = key;
}

int main(int argc, char *argv[])
{
	gpgme_ctx_t	ctx;
	gpgme_error_t	err;
	gpgme_data_t	in, out;
	time_t		start, then = time(NULL);
	int		c, loop;
	char		charset_set = 0;
	char	       *key_filter_string = NULL;

	printf("nasty v" VERSION ", (C) 2005-2022 by folkert@vanheusden.com\n\n");

	start = then;
	srand(then);

	signal(SIGPIPE, SIG_IGN);

	memset(passphrase, 0, min_length);
	memset(passphrase_template, 0, min_length);
	charset[0] = 0x00;
	charset_n = 1;

	if (load_state() == 0)
	{
		if (argc > 1)
		{
			fprintf(stderr, "Cannot use commandline switches when using state-file from a previous run.\n");
			fprintf(stderr, "Delete 'nasty.state' to start with other parameters.\n");
			return 1;
		}

		goto skip_switches;
	}

	while((c = getopt(argc, argv, "a:b:m:c:f:i:k:hv")) != -1)
	{
		switch(c)
		{
			case 'a':
				min_length = atoi(optarg);
				if (min_length < 1)
				{
					fprintf(stderr, "Min. passphrase length is 1\n");
					return 1;
				}
				break;

			case 'b':
				max_length = atoi(optarg);
				if (max_length > MAX_PP_LEN)
				{
					fprintf(stderr, "Max. passphrase length is %d\n", MAX_PP_LEN);
					return 1;
				}
				break;

			case 'm':
				if (strcasecmp(optarg, "incremental") == 0)
					mode = MODE_INCREMENTAL;
				else if (strcasecmp(optarg, "random") == 0)
					mode = MODE_RANDOM;
				else if (strcasecmp(optarg, "file") == 0)
					mode = MODE_FILE;
				else
				{
					fprintf(stderr, "%s: unknown mode\n", optarg);
					return 1;
				}
				break;

			case 'c':
				for(loop=0; loop<strlen(optarg); loop++)
				{
					if (optarg[loop] == 'a')
						add_charset_range('a', 'z');
					else if (optarg[loop] == 'A')
						add_charset_range('A', 'Z');
					else if (optarg[loop] == '0')
						add_charset_range('0', '9');
					else if (optarg[loop] == '.')
						add_charset_range(32, 126);
					else if (optarg[loop] == '+')
						add_charset_range(32, 255);
					else
					{
						fprintf(stderr, "-c: %c is not understood\n", optarg[loop]);
						return 1;
					}

					charset_set = 1;
				}
				break;

			case 'f':
				pp_file_out = optarg;
				break;

			case 'i':
				pp_file_in = optarg;
				break;

			case 'k':
				key_filter_string = optarg;
				break;

			case 'v':
				verbose = 1;
				break;

			case '?':
			case 'h':
				usage();
				return 1;
		}
	}

	if (!charset_set)
	{
		/* set default charset */
		add_charset_range(32, 255);
	}

	if (mode == MODE_FILE)
	{
		if (pp_file_in == NULL)
		{
			fprintf(stderr, "Please select the file to read the passphrases from with '-i'.\n");
			return 1;
		}

		open_pp_file_in();
	}

skip_switches:
	signal(SIGTERM, sighandler);
	signal(SIGINT,  sighandler);
	signal(SIGKILL, sighandler);

	(void)gpgme_check_version(NULL);

	err = gpgme_new(&ctx);
	if (err != GPG_ERR_NO_ERROR) error_exit("gpgme_new failed", err);

	err = gpgme_data_new_from_mem(&in, "1234", 4, 0);
	if (err != GPG_ERR_NO_ERROR) error_exit("gpgme_data_new_from_mem failed", err);
	err = gpgme_data_new(&out);
	if (err != GPG_ERR_NO_ERROR) error_exit("gpgme_data_new failed", err);

	err = gpgme_set_pinentry_mode(ctx, GPGME_PINENTRY_MODE_LOOPBACK);
	if (err != GPG_ERR_NO_ERROR) error_exit("gpgme_set_pinentry_mode failed", err);

	err = gpgme_set_ctx_flag(ctx, "no-symkey-cache", "1");
	if (err != GPG_ERR_NO_ERROR) error_exit("gpgme_set_ctx_flag failed", err);

	gpgme_set_passphrase_cb(ctx, passphrase_cb, NULL);

	gpgme_key_t selected_key;
	filter_keys_and_select_first(ctx, key_filter_string, &selected_key);

	/* instruct gpg-agent to forget any cached passphrase */
	system("gpg-connect-agent reloadagent /bye");

	do
	{
		err = gpgme_op_sign(ctx, in, out, GPGME_SIG_MODE_DETACH);

		if (time(NULL) - then >= 2)
		{
			printf("# tried: %lld (%f per second), last tried: %s          \r", n_done, n_done / (double)(then - start), passphrase);

			then = time(NULL);

			fflush(stdout);
		}
	}
	while(err != 0);

	gpgme_key_unref(selected_key);

	if (err == 0)
		printf("\n\nPassphrase is: %s\n", passphrase);
	else
		error_exit("\n\ngpgme_op_sign failed", err);

	if (pp_file_out)
	{
		FILE *fh = fopen(pp_file_out, "w");
		if (fh)
		{
			fprintf(fh, "passphrase=%s\n", passphrase);
			fclose(fh);
		}
		else
			fprintf(stderr, "Failed to create file '%s': %s\n", pp_file_out, strerror(errno));
	}

	(void)unlink("nasty.state");

	return 0;
}
