/* symcryptrun.c - Tool to call simple symmetric encryption tools. * Copyright (C) 2005, 2007 Free Software Foundation, Inc. * * This file is part of GnuPG. * * GnuPG 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. * * GnuPG 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 . */ /* Sometimes simple encryption tools are already in use for a long time and there is a desire to integrate them into the GnuPG framework. The protocols and encryption methods might be non-standard or not even properly documented, so that a full-fledged encryption tool with an interface like gpg is not doable. This simple wrapper program provides a solution: It operates by calling the encryption/decryption module and providing the passphrase for a key (or even the key directly) using the standard pinentry mechanism through gpg-agent. */ /* This program is invoked in the following way: symcryptrun --class CLASS --program PROGRAM --keyfile KEYFILE \ [--decrypt | --encrypt] For encryption, the plain text must be provided on STDIN, and the ciphertext will be output to STDOUT. For decryption vice versa. CLASS can currently only be "confucius". PROGRAM must be the path to the crypto engine. KEYFILE must contain the secret key, which may be protected by a passphrase. The passphrase is retrieved via the pinentry program. The GPG Agent _must_ be running before starting symcryptrun. The possible exit status codes: 0 Success 1 Some error occured 2 No valid passphrase was provided 3 The operation was canceled by the user Other classes may be added in the future. */ #define SYMC_BAD_PASSPHRASE 2 #define SYMC_CANCELED 3 #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_PTY_H #include #endif #include #include #ifdef HAVE_LOCALE_H #include #endif #ifdef HAVE_LANGINFO_CODESET #include #endif #include #define JNLIB_NEED_LOG_LOGV #include "i18n.h" #include "../common/util.h" #include "mkdtemp.h" /* FIXME: Bah. For spwq_secure_free. */ #define SIMPLE_PWQUERY_IMPLEMENTATION 1 #include "../common/simple-pwquery.h" /* From simple-gettext.c. */ /* We assume to have `unsigned long int' value with at least 32 bits. */ #define HASHWORDBITS 32 /* The so called `hashpjw' function by P.J. Weinberger [see Aho/Sethi/Ullman, COMPILERS: Principles, Techniques and Tools, 1986, 1987 Bell Telephone Laboratories, Inc.] */ static __inline__ ulong hash_string( const char *str_param ) { unsigned long int hval, g; const char *str = str_param; hval = 0; while (*str != '\0') { hval <<= 4; hval += (unsigned long int) *str++; g = hval & ((unsigned long int) 0xf << (HASHWORDBITS - 4)); if (g != 0) { hval ^= g >> (HASHWORDBITS - 8); hval ^= g; } } return hval; } /* Constants to identify the commands and options. */ enum cmd_and_opt_values { aNull = 0, oQuiet = 'q', oVerbose = 'v', oNoVerbose = 500, oOptions, oNoOptions, oLogFile, oHomedir, oClass, oProgram, oKeyfile, oDecrypt, oEncrypt, oInput }; /* The list of commands and options. */ static ARGPARSE_OPTS opts[] = { { 301, NULL, 0, N_("@\nCommands:\n ") }, { oDecrypt, "decrypt", 0, N_("decryption modus") }, { oEncrypt, "encrypt", 0, N_("encryption modus") }, { 302, NULL, 0, N_("@\nOptions:\n ") }, { oClass, "class", 2, N_("tool class (confucius)") }, { oProgram, "program", 2, N_("program filename") }, { oKeyfile, "keyfile", 2, N_("secret key file (required)") }, { oInput, "inputfile", 2, N_("input file name (default stdin)") }, { oVerbose, "verbose", 0, N_("verbose") }, { oQuiet, "quiet", 0, N_("quiet") }, { oLogFile, "log-file", 2, N_("use a log file for the server") }, { oOptions, "options" , 2, N_("|FILE|read options from FILE") }, /* Hidden options. */ { oNoVerbose, "no-verbose", 0, "@" }, { oHomedir, "homedir", 2, "@" }, { oNoOptions, "no-options", 0, "@" },/* shortcut for --options /dev/null */ {0} }; /* We keep all global options in the structure OPT. */ struct { int verbose; /* Verbosity level. */ int quiet; /* Be extra quiet. */ const char *homedir; /* Configuration directory name */ char *class; char *program; char *keyfile; char *input; } opt; /* Print usage information and and provide strings for help. */ static const char * my_strusage (int level) { const char *p; switch (level) { case 11: p = "symcryptrun (GnuPG)"; break; case 13: p = VERSION; break; case 17: p = PRINTABLE_OS_NAME; break; case 19: p = _("Please report bugs to <" PACKAGE_BUGREPORT ">.\n"); break; case 1: case 40: p = _("Usage: symcryptrun [options] (-h for help)"); break; case 41: p = _("Syntax: symcryptrun --class CLASS --program PROGRAM " "--keyfile KEYFILE [options...] COMMAND [inputfile]\n" "Call a simple symmetric encryption tool\n"); break; case 31: p = "\nHome: "; break; case 32: p = opt.homedir; break; case 33: p = "\n"; break; default: p = NULL; break; } return p; } /* This is in the GNU C library in unistd.h. */ #ifndef TEMP_FAILURE_RETRY /* Evaluate EXPRESSION, and repeat as long as it returns -1 with `errno' set to EINTR. */ # define TEMP_FAILURE_RETRY(expression) \ (__extension__ \ ({ long int __result; \ do __result = (long int) (expression); \ while (__result == -1L && errno == EINTR); \ __result; })) #endif /* Include the implementation of map_spwq_error. */ MAP_SPWQ_ERROR_IMPL /* Unlink a file, and shred it if SHRED is true. */ int remove_file (char *name, int shred) { if (!shred) return unlink (name); else { int status; pid_t pid; pid = fork (); if (pid == 0) { /* Child. */ /* -f forces file to be writable, and -u unlinks it afterwards. */ char *args[] = { SHRED, "-uf", name, NULL }; execv (SHRED, args); _exit (127); } else if (pid < 0) { /* Fork failed. */ status = -1; } else { /* Parent. */ if (TEMP_FAILURE_RETRY (waitpid (pid, &status, 0)) != pid) status = -1; } if (!WIFEXITED (status)) { log_error (_("%s on %s aborted with status %i\n"), SHRED, name, status); unlink (name); return 1; } else if (WEXITSTATUS (status)) { log_error (_("%s on %s failed with status %i\n"), SHRED, name, WEXITSTATUS (status)); unlink (name); return 1; } return 0; } } /* Class Confucius. "Don't worry that other people don't know you; worry that you don't know other people." Analects--1.16. */ /* Create temporary directory with mode 0700. Returns a dynamically allocated string with the filename of the directory. */ static char * confucius_mktmpdir (void) { char *name; name = strdup ("/tmp/gpg-XXXXXX"); if (!name || !mkdtemp (name)) { log_error (_("can't create temporary directory `%s': %s\n"), name?name:"", strerror (errno)); return NULL; } return name; } /* Buffer size for I/O operations. */ #define CONFUCIUS_BUFSIZE 4096 /* Buffer size for output lines. */ #define CONFUCIUS_LINESIZE 4096 /* Copy the file IN to OUT, either of which may be "-". If PLAIN is true, and the copying fails, and OUT is not STDOUT, then shred the file instead unlinking it. */ static int confucius_copy_file (char *infile, char *outfile, int plain) { FILE *in; int in_is_stdin = 0; FILE *out; int out_is_stdout = 0; char data[CONFUCIUS_BUFSIZE]; ssize_t data_len; if (infile[0] == '-' && infile[1] == '\0') { /* FIXME: Is stdin in binary mode? */ in = stdin; in_is_stdin = 1; } else { in = fopen (infile, "rb"); if (!in) { log_error (_("could not open %s for writing: %s\n"), infile, strerror (errno)); return 1; } } if (outfile[0] == '-' && outfile[1] == '\0') { /* FIXME: Is stdout in binary mode? */ out = stdout; out_is_stdout = 1; } else { out = fopen (outfile, "wb"); if (!out) { log_error (_("could not open %s for writing: %s\n"), infile, strerror (errno)); return 1; } } /* Now copy the data. */ while ((data_len = fread (data, 1, sizeof (data), in)) > 0) { if (fwrite (data, 1, data_len, out) != data_len) { log_error (_("error writing to %s: %s\n"), outfile, strerror (errno)); goto copy_err; } } if (data_len < 0 || ferror (in)) { log_error (_("error reading from %s: %s\n"), infile, strerror (errno)); goto copy_err; } /* Close IN if appropriate. */ if (!in_is_stdin && fclose (in) && ferror (in)) { log_error (_("error closing %s: %s\n"), infile, strerror (errno)); goto copy_err; } /* Close OUT if appropriate. */ if (!out_is_stdout && fclose (out) && ferror (out)) { log_error (_("error closing %s: %s\n"), infile, strerror (errno)); goto copy_err; } return 0; copy_err: if (!out_is_stdout) remove_file (outfile, plain); return 1; } /* Get a passphrase in secure storage (if possible). If AGAIN is true, then this is a repeated attempt. If CANCELED is not a null pointer, it will be set to true or false, depending on if the user canceled the operation or not. On error (including cancelation), a null pointer is returned. The passphrase must be deallocated with confucius_drop_pass. CACHEID is the ID to be used for passphrase caching and can be NULL to disable caching. */ char * confucius_get_pass (const char *cacheid, int again, int *canceled) { int err; char *pw; #ifdef HAVE_LANGINFO_CODESET char *orig_codeset = NULL; #endif if (canceled) *canceled = 0; #ifdef ENABLE_NLS /* The Assuan agent protocol requires us to transmit utf-8 strings */ orig_codeset = bind_textdomain_codeset (PACKAGE_GT, NULL); #ifdef HAVE_LANGINFO_CODESET if (!orig_codeset) orig_codeset = nl_langinfo (CODESET); #endif if (orig_codeset && !strcmp (orig_codeset, "UTF-8")) orig_codeset = NULL; if (orig_codeset) { /* We only switch when we are able to restore the codeset later. */ orig_codeset = xstrdup (orig_codeset); if (!bind_textdomain_codeset (PACKAGE_GT, "utf-8")) { xfree (orig_codeset); orig_codeset = NULL; } } #endif pw = simple_pwquery (cacheid, again ? _("does not match - try again"):NULL, _("Passphrase:"), NULL, 0, &err); err = map_spwq_error (err); #ifdef ENABLE_NLS if (orig_codeset) { bind_textdomain_codeset (PACKAGE_GT, orig_codeset); xfree (orig_codeset); } #endif if (!pw) { if (err) log_error (_("error while asking for the passphrase: %s\n"), gpg_strerror (err)); else { log_info (_("cancelled\n")); if (canceled) *canceled = 1; } } return pw; } /* Drop a passphrase retrieved with confucius_get_pass. */ void confucius_drop_pass (char *pass) { if (pass) spwq_secure_free (pass); } /* Run a confucius crypto engine. If MODE is oEncrypt, encryption is requested. If it is oDecrypt, decryption is requested. INFILE and OUTFILE are the temporary files used in the process. */ int confucius_process (int mode, char *infile, char *outfile, int argc, char *argv[]) { char **args; int cstderr[2]; int master; int slave; int res; pid_t pid; pid_t wpid; int tries = 0; char cacheid[40]; signal (SIGPIPE, SIG_IGN); if (!opt.program) { log_error (_("no --program option provided\n")); return 1; } if (mode != oDecrypt && mode != oEncrypt) { log_error (_("only --decrypt and --encrypt are supported\n")); return 1; } if (!opt.keyfile) { log_error (_("no --keyfile option provided\n")); return 1; } /* Generate a hash from the keyfile name for caching. */ snprintf (cacheid, sizeof (cacheid), "confucius:%lu", hash_string (opt.keyfile)); cacheid[sizeof (cacheid) - 1] = '\0'; args = malloc (sizeof (char *) * (10 + argc)); if (!args) { log_error (_("cannot allocate args vector\n")); return 1; } args[0] = opt.program; args[1] = (mode == oEncrypt) ? "-m1" : "-m2"; args[2] = "-q"; args[3] = infile; args[4] = "-z"; args[5] = outfile; args[6] = "-s"; args[7] = opt.keyfile; args[8] = (mode == oEncrypt) ? "-af" : "-f"; args[9 + argc] = NULL; while (argc--) args[9 + argc] = argv[argc]; if (pipe (cstderr) < 0) { log_error (_("could not create pipe: %s\n"), strerror (errno)); free (args); return 1; } if (openpty (&master, &slave, NULL, NULL, NULL) == -1) { log_error (_("could not create pty: %s\n"), strerror (errno)); close (cstderr[0]); close (cstderr[1]); free (args); return -1; } /* We don't want to deal with the worst case scenarios. */ assert (master > 2); assert (slave > 2); assert (cstderr[0] > 2); assert (cstderr[1] > 2); pid = fork (); if (pid < 0) { log_error (_("could not fork: %s\n"), strerror (errno)); close (master); close (slave); close (cstderr[0]); close (cstderr[1]); free (args); return 1; } else if (pid == 0) { /* Child. */ /* Close the parent ends. */ close (master); close (cstderr[0]); /* Change controlling terminal. */ if (login_tty (slave)) { /* It's too early to output a debug message. */ _exit (1); } dup2 (cstderr[1], 2); close (cstderr[1]); /* Now kick off the engine program. */ execv (opt.program, args); log_error (_("execv failed: %s\n"), strerror (errno)); _exit (1); } else { /* Parent. */ char buffer[CONFUCIUS_LINESIZE]; int buffer_len = 0; fd_set fds; int slave_closed = 0; int stderr_closed = 0; close (slave); close (cstderr[1]); free (args); /* Listen on the output FDs. */ do { FD_ZERO (&fds); if (!slave_closed) FD_SET (master, &fds); if (!stderr_closed) FD_SET (cstderr[0], &fds); res = select (FD_SETSIZE, &fds, NULL, NULL, NULL); if (res < 0) { log_error (_("select failed: %s\n"), strerror (errno)); kill (pid, SIGTERM); close (master); close (cstderr[0]); return 1; } if (FD_ISSET (cstderr[0], &fds)) { /* We got some output on stderr. This is just passed through via the logging facility. */ res = read (cstderr[0], &buffer[buffer_len], sizeof (buffer) - buffer_len - 1); if (res < 0) { log_error (_("read failed: %s\n"), strerror (errno)); kill (pid, SIGTERM); close (master); close (cstderr[0]); return 1; } else { char *newline; buffer_len += res; for (;;) { buffer[buffer_len] = '\0'; newline = strchr (buffer, '\n'); if (newline) { *newline = '\0'; log_error ("%s\n", buffer); buffer_len -= newline + 1 - buffer; memmove (buffer, newline + 1, buffer_len); } else if (buffer_len == sizeof (buffer) - 1) { /* Overflow. */ log_error ("%s\n", buffer); buffer_len = 0; } else break; } if (res == 0) stderr_closed = 1; } } else if (FD_ISSET (master, &fds)) { char data[512]; res = read (master, data, sizeof (data)); if (res < 0) { if (errno == EIO) { /* Slave-side close leads to readable fd and EIO. */ slave_closed = 1; } else { log_error (_("pty read failed: %s\n"), strerror (errno)); kill (pid, SIGTERM); close (master); close (cstderr[0]); return 1; } } else if (res == 0) /* This never seems to be what happens on slave-side close. */ slave_closed = 1; else { /* Check for password prompt. */ if (data[res - 1] == ':') { char *pass; int canceled; /* If this is not the first attempt, the passphrase seems to be wrong, so clear the cache. */ if (tries) simple_pwclear (cacheid); pass = confucius_get_pass (cacheid, tries ? 1 : 0, &canceled); if (!pass) { kill (pid, SIGTERM); close (master); close (cstderr[0]); return canceled ? SYMC_CANCELED : 1; } write (master, pass, strlen (pass)); write (master, "\n", 1); confucius_drop_pass (pass); tries++; } } } } while (!stderr_closed || !slave_closed); close (master); close (cstderr[0]); wpid = waitpid (pid, &res, 0); if (wpid < 0) { log_error (_("waitpid failed: %s\n"), strerror (errno)); kill (pid, SIGTERM); /* State of cached password is unclear. Just remove it. */ simple_pwclear (cacheid); return 1; } else { /* Shouldn't happen, as we don't use WNOHANG. */ assert (wpid != 0); if (!WIFEXITED (res)) { log_error (_("child aborted with status %i\n"), res); /* State of cached password is unclear. Just remove it. */ simple_pwclear (cacheid); return 1; } if (WEXITSTATUS (res)) { /* The passphrase was wrong. Remove it from the cache. */ simple_pwclear (cacheid); /* We probably exceeded our number of attempts at guessing the password. */ if (tries >= 3) return SYMC_BAD_PASSPHRASE; else return 1; } return 0; } } /* Not reached. */ } /* Class confucius main program. If MODE is oEncrypt, encryption is requested. If it is oDecrypt, decryption is requested. The other parameters are taken from the global option data. */ int confucius_main (int mode, int argc, char *argv[]) { int res; char *tmpdir; char *infile; int infile_from_stdin = 0; char *outfile; tmpdir = confucius_mktmpdir (); if (!tmpdir) return 1; if (opt.input && !(opt.input[0] == '-' && opt.input[1] == '\0')) infile = xstrdup (opt.input); else { infile_from_stdin = 1; /* TMPDIR + "/" + "in" + "\0". */ infile = malloc (strlen (tmpdir) + 1 + 2 + 1); if (!infile) { log_error (_("cannot allocate infile string: %s\n"), strerror (errno)); rmdir (tmpdir); return 1; } strcpy (infile, tmpdir); strcat (infile, "/in"); } /* TMPDIR + "/" + "out" + "\0". */ outfile = malloc (strlen (tmpdir) + 1 + 3 + 1); if (!outfile) { log_error (_("cannot allocate outfile string: %s\n"), strerror (errno)); free (infile); rmdir (tmpdir); return 1; } strcpy (outfile, tmpdir); strcat (outfile, "/out"); if (infile_from_stdin) { /* Create INFILE and fill it with content. */ res = confucius_copy_file ("-", infile, mode == oEncrypt); if (res) { free (outfile); free (infile); rmdir (tmpdir); return res; } } /* Run the engine and thus create the output file, handling passphrase retrieval. */ res = confucius_process (mode, infile, outfile, argc, argv); if (res) { remove_file (outfile, mode == oDecrypt); if (infile_from_stdin) remove_file (infile, mode == oEncrypt); free (outfile); free (infile); rmdir (tmpdir); return res; } /* Dump the output file to stdout. */ res = confucius_copy_file (outfile, "-", mode == oDecrypt); if (res) { remove_file (outfile, mode == oDecrypt); if (infile_from_stdin) remove_file (infile, mode == oEncrypt); free (outfile); free (infile); rmdir (tmpdir); return res; } remove_file (outfile, mode == oDecrypt); if (infile_from_stdin) remove_file (infile, mode == oEncrypt); free (outfile); free (infile); rmdir (tmpdir); return 0; } /* symcryptrun's entry point. */ int main (int argc, char **argv) { ARGPARSE_ARGS pargs; int orig_argc; char **orig_argv; FILE *configfp = NULL; char *configname = NULL; unsigned configlineno; int mode = 0; int res; char *logfile = NULL; int default_config = 1; set_strusage (my_strusage); log_set_prefix ("symcryptrun", 1); /* Make sure that our subsystems are ready. */ init_common_subsystems (); i18n_init(); opt.homedir = default_homedir (); /* Check whether we have a config file given on the commandline */ orig_argc = argc; orig_argv = argv; pargs.argc = &argc; pargs.argv = &argv; pargs.flags= 1|(1<<6); /* do not remove the args, ignore version */ while (arg_parse( &pargs, opts)) { if (pargs.r_opt == oOptions) { /* Yes there is one, so we do not try the default one, but read the option file when it is encountered at the commandline */ default_config = 0; } else if (pargs.r_opt == oNoOptions) default_config = 0; /* --no-options */ else if (pargs.r_opt == oHomedir) opt.homedir = pargs.r.ret_str; } if (default_config) configname = make_filename (opt.homedir, "symcryptrun.conf", NULL ); argc = orig_argc; argv = orig_argv; pargs.argc = &argc; pargs.argv = &argv; pargs.flags= 1; /* do not remove the args */ next_pass: if (configname) { configlineno = 0; configfp = fopen (configname, "r"); if (!configfp) { if (!default_config) { log_error (_("option file `%s': %s\n"), configname, strerror(errno) ); exit(1); } xfree (configname); configname = NULL; } default_config = 0; } /* Parse the command line. */ while (optfile_parse (configfp, configname, &configlineno, &pargs, opts)) { switch (pargs.r_opt) { case oDecrypt: mode = oDecrypt; break; case oEncrypt: mode = oEncrypt; break; case oQuiet: opt.quiet = 1; break; case oVerbose: opt.verbose++; break; case oNoVerbose: opt.verbose = 0; break; case oClass: opt.class = pargs.r.ret_str; break; case oProgram: opt.program = pargs.r.ret_str; break; case oKeyfile: opt.keyfile = pargs.r.ret_str; break; case oInput: opt.input = pargs.r.ret_str; break; case oLogFile: logfile = pargs.r.ret_str; break; case oOptions: /* Config files may not be nested (silently ignore them) */ if (!configfp) { xfree(configname); configname = xstrdup(pargs.r.ret_str); goto next_pass; } break; case oNoOptions: break; /* no-options */ case oHomedir: /* Ignore this option here. */; break; default : pargs.err = configfp? 1:2; break; } } if (configfp) { fclose( configfp ); configfp = NULL; configname = NULL; goto next_pass; } xfree (configname); configname = NULL; if (!mode) log_error (_("either %s or %s must be given\n"), "--decrypt", "--encrypt"); if (log_get_errorcount (0)) exit (1); if (logfile) log_set_file (logfile); gcry_control (GCRYCTL_SUSPEND_SECMEM_WARN); if (!gcry_check_version (NEED_LIBGCRYPT_VERSION) ) { log_fatal (_("%s is too old (need %s, have %s)\n"), "libgcrypt", NEED_LIBGCRYPT_VERSION, gcry_check_version (NULL) ); } setup_libgcrypt_logging (); gcry_control (GCRYCTL_INIT_SECMEM, 16384, 0); /* Tell simple-pwquery about the the standard socket name. */ { char *tmp = make_filename (opt.homedir, "S.gpg-agent", NULL); simple_pw_set_socket (tmp); xfree (tmp); } if (!opt.class) { log_error (_("no class provided\n")); res = 1; } else if (!strcmp (opt.class, "confucius")) { res = confucius_main (mode, argc, argv); } else { log_error (_("class %s is not supported\n"), opt.class); res = 1; } return res; }