summaryrefslogtreecommitdiffstats
path: root/tools/gpgtar.c
diff options
context:
space:
mode:
Diffstat (limited to 'tools/gpgtar.c')
-rw-r--r--tools/gpgtar.c614
1 files changed, 614 insertions, 0 deletions
diff --git a/tools/gpgtar.c b/tools/gpgtar.c
new file mode 100644
index 0000000..c2f24ec
--- /dev/null
+++ b/tools/gpgtar.c
@@ -0,0 +1,614 @@
+/* gpgtar.c - A simple TAR implementation mainly useful for Windows.
+ * Copyright (C) 2010 Free Software Foundation, Inc.
+ * Copyright (C) 2020 g10 Code GmbH
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/* GnuPG comes with a shell script gpg-zip which creates archive files
+ in the same format as PGP Zip, which is actually a USTAR format.
+ That is fine and works nicely on all Unices but for Windows we
+ don't have a compatible shell and the supply of tar programs is
+ limited. Given that we need just a few tar option and it is an
+ open question how many Unix concepts are to be mapped to Windows,
+ we might as well write our own little tar customized for use with
+ gpg. So here we go. */
+
+#include <config.h>
+#include <ctype.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+
+#define INCLUDED_BY_MAIN_MODULE 1
+#include "../common/util.h"
+#include "../common/i18n.h"
+#include "../common/sysutils.h"
+#include "../common/openpgpdefs.h"
+#include "../common/init.h"
+#include "../common/strlist.h"
+
+#include "gpgtar.h"
+
+
+/* Constants to identify the commands and options. */
+enum cmd_and_opt_values
+ {
+ aNull = 0,
+ aCreate = 600,
+ aExtract,
+ aEncrypt = 'e',
+ aDecrypt = 'd',
+ aSign = 's',
+ aList = 't',
+
+ oSymmetric = 'c',
+ oRecipient = 'r',
+ oUser = 'u',
+ oOutput = 'o',
+ oDirectory = 'C',
+ oQuiet = 'q',
+ oVerbose = 'v',
+ oFilesFrom = 'T',
+ oNoVerbose = 500,
+
+ aSignEncrypt,
+ oGpgProgram,
+ oSkipCrypto,
+ oOpenPGP,
+ oCMS,
+ oSetFilename,
+ oNull,
+ oUtf8Strings,
+
+ /* Compatibility with gpg-zip. */
+ oGpgArgs,
+ oTarArgs,
+
+ /* Debugging. */
+ oDryRun,
+ };
+
+
+/* The list of commands and options. */
+static ARGPARSE_OPTS opts[] = {
+ ARGPARSE_group (300, N_("@Commands:\n ")),
+
+ ARGPARSE_c (aCreate, "create", N_("create an archive")),
+ ARGPARSE_c (aExtract, "extract", N_("extract an archive")),
+ ARGPARSE_c (aEncrypt, "encrypt", N_("create an encrypted archive")),
+ ARGPARSE_c (aDecrypt, "decrypt", N_("extract an encrypted archive")),
+ ARGPARSE_c (aSign, "sign", N_("create a signed archive")),
+ ARGPARSE_c (aList, "list-archive", N_("list an archive")),
+
+ ARGPARSE_group (301, N_("@\nOptions:\n ")),
+
+ ARGPARSE_s_n (oSymmetric, "symmetric", N_("use symmetric encryption")),
+ ARGPARSE_s_s (oRecipient, "recipient", N_("|USER-ID|encrypt for USER-ID")),
+ ARGPARSE_s_s (oUser, "local-user",
+ N_("|USER-ID|use USER-ID to sign or decrypt")),
+ ARGPARSE_s_s (oOutput, "output", N_("|FILE|write output to FILE")),
+ ARGPARSE_s_n (oVerbose, "verbose", N_("verbose")),
+ ARGPARSE_s_n (oQuiet, "quiet", N_("be somewhat more quiet")),
+ ARGPARSE_s_s (oGpgProgram, "gpg", "@"),
+ ARGPARSE_s_n (oSkipCrypto, "skip-crypto", N_("skip the crypto processing")),
+ ARGPARSE_s_n (oDryRun, "dry-run", N_("do not make any changes")),
+ ARGPARSE_s_s (oSetFilename, "set-filename", "@"),
+ ARGPARSE_s_n (oOpenPGP, "openpgp", "@"),
+ ARGPARSE_s_n (oCMS, "cms", "@"),
+
+ ARGPARSE_group (302, N_("@\nTar options:\n ")),
+
+ ARGPARSE_s_s (oDirectory, "directory",
+ N_("|DIRECTORY|change to DIRECTORY first")),
+ ARGPARSE_s_s (oFilesFrom, "files-from",
+ N_("|FILE|get names to create from FILE")),
+ ARGPARSE_s_n (oNull, "null", N_("-T reads null-terminated names")),
+#ifdef HAVE_W32_SYSTEM
+ ARGPARSE_s_n (oUtf8Strings, "utf8-strings",
+ N_("-T reads UTF-8 encoded names")),
+#else
+ ARGPARSE_s_n (oUtf8Strings, "utf8-strings", "@"),
+#endif
+
+ ARGPARSE_s_s (oGpgArgs, "gpg-args", "@"),
+ ARGPARSE_s_s (oTarArgs, "tar-args", "@"),
+
+ ARGPARSE_end ()
+};
+
+
+/* The list of commands and options for tar that we understand. */
+static ARGPARSE_OPTS tar_opts[] = {
+ ARGPARSE_s_s (oDirectory, "directory",
+ N_("|DIRECTORY|extract files into DIRECTORY")),
+ ARGPARSE_s_s (oFilesFrom, "files-from",
+ N_("|FILE|get names to create from FILE")),
+ ARGPARSE_s_n (oNull, "null", N_("-T reads null-terminated names")),
+
+ ARGPARSE_end ()
+};
+
+
+/* Global flags. */
+static enum cmd_and_opt_values cmd = 0;
+static int skip_crypto = 0;
+static const char *files_from = NULL;
+static int null_names = 0;
+
+
+
+
+/* Print usage information and provide strings for help. */
+static const char *
+my_strusage( int level )
+{
+ const char *p;
+
+ switch (level)
+ {
+ case 9: p = "GPL-3.0-or-later"; break;
+ case 11: p = "@GPGTAR@ (@GNUPG@)";
+ break;
+ case 13: p = VERSION; break;
+ case 14: p = GNUPG_DEF_COPYRIGHT_LINE; break;
+ case 17: p = PRINTABLE_OS_NAME; break;
+ case 19: p = _("Please report bugs to <@EMAIL@>.\n"); break;
+
+ case 1:
+ case 40:
+ p = _("Usage: gpgtar [options] [files] [directories] (-h for help)");
+ break;
+ case 41:
+ p = _("Syntax: gpgtar [options] [files] [directories]\n"
+ "Encrypt or sign files into an archive\n");
+ break;
+
+ default: p = NULL; break;
+ }
+ return p;
+}
+
+
+static void
+set_cmd (enum cmd_and_opt_values *ret_cmd, enum cmd_and_opt_values new_cmd)
+{
+ enum cmd_and_opt_values c = *ret_cmd;
+
+ if (!c || c == new_cmd)
+ c = new_cmd;
+ else if (c == aSign && new_cmd == aEncrypt)
+ c = aSignEncrypt;
+ else if (c == aEncrypt && new_cmd == aSign)
+ c = aSignEncrypt;
+ else
+ {
+ log_error (_("conflicting commands\n"));
+ exit (2);
+ }
+
+ *ret_cmd = c;
+}
+
+
+
+/* Shell-like argument splitting.
+
+ For compatibility with gpg-zip we accept arguments for GnuPG and
+ tar given as a string argument to '--gpg-args' and '--tar-args'.
+ gpg-zip was implemented as a Bourne Shell script, and therefore, we
+ need to split the string the same way the shell would. */
+static int
+shell_parse_stringlist (const char *str, strlist_t *r_list)
+{
+ strlist_t list = NULL;
+ const char *s = str;
+ char quoted = 0;
+ char arg[1024];
+ char *p = arg;
+#define addchar(c) \
+ do { if (p - arg + 2 < sizeof arg) *p++ = (c); else return 1; } while (0)
+#define addargument() \
+ do { \
+ if (p > arg) \
+ { \
+ *p = 0; \
+ append_to_strlist (&list, arg); \
+ p = arg; \
+ } \
+ } while (0)
+
+#define unquoted 0
+#define singlequote '\''
+#define doublequote '"'
+
+ for (; *s; s++)
+ {
+ switch (quoted)
+ {
+ case unquoted:
+ if (isspace (*s))
+ addargument ();
+ else if (*s == singlequote || *s == doublequote)
+ quoted = *s;
+ else
+ addchar (*s);
+ break;
+
+ case singlequote:
+ if (*s == singlequote)
+ quoted = unquoted;
+ else
+ addchar (*s);
+ break;
+
+ case doublequote:
+ assert (s > str || !"cannot be quoted at first char");
+ if (*s == doublequote && *(s - 1) != '\\')
+ quoted = unquoted;
+ else
+ addchar (*s);
+ break;
+
+ default:
+ assert (! "reached");
+ }
+ }
+
+ /* Append the last argument. */
+ addargument ();
+
+#undef doublequote
+#undef singlequote
+#undef unquoted
+#undef addargument
+#undef addchar
+ *r_list = list;
+ return 0;
+}
+
+
+/* Like shell_parse_stringlist, but returns an argv vector
+ instead of a strlist. */
+static int
+shell_parse_argv (const char *s, int *r_argc, char ***r_argv)
+{
+ int i;
+ strlist_t list;
+
+ if (shell_parse_stringlist (s, &list))
+ return 1;
+
+ *r_argc = strlist_length (list);
+ *r_argv = xtrycalloc (*r_argc, sizeof **r_argv);
+ if (*r_argv == NULL)
+ return 1;
+
+ for (i = 0; list; i++)
+ {
+ gpgrt_annotate_leaked_object (list);
+ (*r_argv)[i] = list->d;
+ list = list->next;
+ }
+ gpgrt_annotate_leaked_object (*r_argv);
+ return 0;
+}
+
+
+
+/* Command line parsing. */
+static void
+parse_arguments (ARGPARSE_ARGS *pargs, ARGPARSE_OPTS *popts)
+{
+ int no_more_options = 0;
+
+ while (!no_more_options && gnupg_argparse (NULL, pargs, popts))
+ {
+ switch (pargs->r_opt)
+ {
+ case oOutput: opt.outfile = pargs->r.ret_str; break;
+ case oDirectory: opt.directory = pargs->r.ret_str; break;
+ case oSetFilename: opt.filename = pargs->r.ret_str; break;
+ case oQuiet: opt.quiet = 1; break;
+ case oVerbose: opt.verbose++; break;
+ case oNoVerbose: opt.verbose = 0; break;
+ case oFilesFrom: files_from = pargs->r.ret_str; break;
+ case oNull: null_names = 1; break;
+ case oUtf8Strings: opt.utf8strings = 1; break;
+
+ case aList:
+ case aDecrypt:
+ case aEncrypt:
+ case aSign:
+ set_cmd (&cmd, pargs->r_opt);
+ break;
+
+ case aCreate:
+ set_cmd (&cmd, aEncrypt);
+ skip_crypto = 1;
+ break;
+
+ case aExtract:
+ set_cmd (&cmd, aDecrypt);
+ skip_crypto = 1;
+ break;
+
+ case oRecipient:
+ add_to_strlist (&opt.recipients, pargs->r.ret_str);
+ break;
+
+ case oUser:
+ opt.user = pargs->r.ret_str;
+ break;
+
+ case oSymmetric:
+ set_cmd (&cmd, aEncrypt);
+ opt.symmetric = 1;
+ break;
+
+ case oGpgProgram:
+ opt.gpg_program = pargs->r.ret_str;
+ break;
+
+ case oSkipCrypto:
+ skip_crypto = 1;
+ break;
+
+ case oOpenPGP: /* Dummy option for now. */ break;
+ case oCMS: /* Dummy option for now. */ break;
+
+ case oGpgArgs:;
+ {
+ strlist_t list;
+ if (shell_parse_stringlist (pargs->r.ret_str, &list))
+ log_error ("failed to parse gpg arguments '%s'\n",
+ pargs->r.ret_str);
+ else
+ {
+ if (opt.gpg_arguments)
+ strlist_last (opt.gpg_arguments)->next = list;
+ else
+ opt.gpg_arguments = list;
+ }
+ }
+ break;
+
+ case oTarArgs:
+ {
+ int tar_argc;
+ char **tar_argv;
+
+ if (shell_parse_argv (pargs->r.ret_str, &tar_argc, &tar_argv))
+ log_error ("failed to parse tar arguments '%s'\n",
+ pargs->r.ret_str);
+ else
+ {
+ ARGPARSE_ARGS tar_args;
+ tar_args.argc = &tar_argc;
+ tar_args.argv = &tar_argv;
+ tar_args.flags = ARGPARSE_FLAG_ARG0;
+ parse_arguments (&tar_args, tar_opts);
+ gnupg_argparse (NULL, &tar_args, NULL);
+ if (tar_args.err)
+ log_error ("unsupported tar arguments '%s'\n",
+ pargs->r.ret_str);
+ pargs->err = tar_args.err;
+ }
+ }
+ break;
+
+ case oDryRun:
+ opt.dry_run = 1;
+ break;
+
+ default: pargs->err = 2; break;
+ }
+ }
+}
+
+
+/* gpgtar main. */
+int
+main (int argc, char **argv)
+{
+ gpg_error_t err;
+ const char *fname;
+ ARGPARSE_ARGS pargs;
+
+ gnupg_reopen_std (GPGTAR_NAME);
+ set_strusage (my_strusage);
+ log_set_prefix (GPGTAR_NAME, GPGRT_LOG_WITH_PREFIX);
+
+ /* Make sure that our subsystems are ready. */
+ i18n_init();
+ init_common_subsystems (&argc, &argv);
+
+ log_assert (sizeof (struct ustar_raw_header) == 512);
+
+ /* Parse the command line. */
+ pargs.argc = &argc;
+ pargs.argv = &argv;
+ pargs.flags = ARGPARSE_FLAG_KEEP;
+ parse_arguments (&pargs, opts);
+ gnupg_argparse (NULL, &pargs, NULL);
+
+ if (log_get_errorcount (0))
+ exit (2);
+
+ /* Print a warning if an argument looks like an option. */
+ if (!opt.quiet && !(pargs.flags & ARGPARSE_FLAG_STOP_SEEN))
+ {
+ int i;
+
+ for (i=0; i < argc; i++)
+ if (argv[i][0] == '-' && argv[i][1] == '-')
+ log_info (_("NOTE: '%s' is not considered an option\n"), argv[i]);
+ }
+
+ if (! opt.gpg_program)
+ opt.gpg_program = gnupg_module_name (GNUPG_MODULE_NAME_GPG);
+
+ if (opt.verbose > 1)
+ opt.debug_level = 1024;
+
+ switch (cmd)
+ {
+ case aList:
+ if (argc > 1)
+ usage (1);
+ fname = argc ? *argv : NULL;
+ if (opt.filename)
+ log_info ("note: ignoring option --set-filename\n");
+ if (files_from)
+ log_info ("note: ignoring option --files-from\n");
+ err = gpgtar_list (fname, !skip_crypto);
+ if (err && log_get_errorcount (0) == 0)
+ log_error ("listing archive failed: %s\n", gpg_strerror (err));
+ break;
+
+ case aEncrypt:
+ case aSign:
+ case aSignEncrypt:
+ if ((!argc && !files_from)
+ || (argc && files_from))
+ usage (1);
+ if (opt.filename)
+ log_info ("note: ignoring option --set-filename\n");
+ err = gpgtar_create (files_from? NULL : argv,
+ files_from,
+ null_names,
+ !skip_crypto
+ && (cmd == aEncrypt || cmd == aSignEncrypt),
+ cmd == aSign || cmd == aSignEncrypt);
+ if (err && log_get_errorcount (0) == 0)
+ log_error ("creating archive failed: %s\n", gpg_strerror (err));
+ break;
+
+ case aDecrypt:
+ if (argc != 1)
+ usage (1);
+ if (opt.outfile)
+ log_info ("note: ignoring option --output\n");
+ if (files_from)
+ log_info ("note: ignoring option --files-from\n");
+ fname = argc ? *argv : NULL;
+ err = gpgtar_extract (fname, !skip_crypto);
+ if (err && log_get_errorcount (0) == 0)
+ log_error ("extracting archive failed: %s\n", gpg_strerror (err));
+ break;
+
+ default:
+ log_error (_("invalid command (there is no implicit command)\n"));
+ break;
+ }
+
+ return log_get_errorcount (0)? 1:0;
+}
+
+
+/* Read the next record from STREAM. RECORD is a buffer provided by
+ the caller and must be at leadt of size RECORDSIZE. The function
+ return 0 on success and error code on failure; a diagnostic
+ printed as well. Note that there is no need for an EOF indicator
+ because a tarball has an explicit EOF record. */
+gpg_error_t
+read_record (estream_t stream, void *record)
+{
+ gpg_error_t err;
+ size_t nread;
+
+ nread = es_fread (record, 1, RECORDSIZE, stream);
+ if (nread != RECORDSIZE)
+ {
+ err = gpg_error_from_syserror ();
+ if (es_ferror (stream))
+ log_error ("error reading '%s': %s\n",
+ es_fname_get (stream), gpg_strerror (err));
+ else
+ log_error ("error reading '%s': premature EOF "
+ "(size of last record: %zu)\n",
+ es_fname_get (stream), nread);
+ }
+ else
+ err = 0;
+
+ return err;
+}
+
+
+/* Write the RECORD of size RECORDSIZE to STREAM. FILENAME is the
+ name of the file used for diagnostics. */
+gpg_error_t
+write_record (estream_t stream, const void *record)
+{
+ gpg_error_t err;
+ size_t nwritten;
+
+ nwritten = es_fwrite (record, 1, RECORDSIZE, stream);
+ if (nwritten != RECORDSIZE)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error writing '%s': %s\n",
+ es_fname_get (stream), gpg_strerror (err));
+ }
+ else
+ err = 0;
+
+ return err;
+}
+
+
+/* Return true if FP is an unarmored OpenPGP message. Note that this
+ function reads a few bytes from FP but pushes them back. */
+#if 0
+static int
+openpgp_message_p (estream_t fp)
+{
+ int ctb;
+
+ ctb = es_getc (fp);
+ if (ctb != EOF)
+ {
+ if (es_ungetc (ctb, fp))
+ log_fatal ("error ungetting first byte: %s\n",
+ gpg_strerror (gpg_error_from_syserror ()));
+
+ if ((ctb & 0x80))
+ {
+ switch ((ctb & 0x40) ? (ctb & 0x3f) : ((ctb>>2)&0xf))
+ {
+ case PKT_MARKER:
+ case PKT_SYMKEY_ENC:
+ case PKT_ONEPASS_SIG:
+ case PKT_PUBKEY_ENC:
+ case PKT_SIGNATURE:
+ case PKT_COMMENT:
+ case PKT_OLD_COMMENT:
+ case PKT_PLAINTEXT:
+ case PKT_COMPRESSED:
+ case PKT_ENCRYPTED:
+ return 1; /* Yes, this seems to be an OpenPGP message. */
+ default:
+ break;
+ }
+ }
+ }
+ return 0;
+}
+#endif