/* sh-cmd.c - The Assuan server for g13-syshelp * Copyright (C) 2015 Werner Koch * * 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 . */ #include #include #include #include #include #include #include #include "g13-syshelp.h" #include #include "../common/i18n.h" #include "../common/asshelp.h" #include "keyblob.h" /* Local data for this server module. A pointer to this is stored in the CTRL object of each connection. */ struct server_local_s { /* The Assuan context we are working on. */ assuan_context_t assuan_ctx; /* The malloced name of the device. */ char *devicename; /* A stream open for read of the device set by the DEVICE command or NULL if no DEVICE command has been used. */ estream_t devicefp; }; /* Local prototypes. */ /* Helper functions. */ /* Set an error and a description. */ #define set_error(e,t) assuan_set_error (ctx, gpg_error (e), (t)) #define set_error_fail_cmd() set_error (GPG_ERR_NOT_INITIALIZED, \ "not called via userv or unknown user") /* Skip over options. Blanks after the options are also removed. */ static char * skip_options (const char *line) { while (spacep (line)) line++; while ( *line == '-' && line[1] == '-' ) { while (*line && !spacep (line)) line++; while (spacep (line)) line++; } return (char*)line; } /* Check whether the option NAME appears in LINE. */ /* static int */ /* has_option (const char *line, const char *name) */ /* { */ /* const char *s; */ /* int n = strlen (name); */ /* s = strstr (line, name); */ /* if (s && s >= skip_options (line)) */ /* return 0; */ /* return (s && (s == line || spacep (s-1)) && (!s[n] || spacep (s+n))); */ /* } */ /* Helper to print a message while leaving a command. */ static gpg_error_t leave_cmd (assuan_context_t ctx, gpg_error_t err) { if (err) { const char *name = assuan_get_command_name (ctx); if (!name) name = "?"; if (gpg_err_source (err) == GPG_ERR_SOURCE_DEFAULT) log_error ("command '%s' failed: %s\n", name, gpg_strerror (err)); else log_error ("command '%s' failed: %s <%s>\n", name, gpg_strerror (err), gpg_strsource (err)); } return err; } /* The handler for Assuan OPTION commands. */ static gpg_error_t option_handler (assuan_context_t ctx, const char *key, const char *value) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; (void)ctrl; (void)key; (void)value; if (ctrl->fail_all_cmds) err = set_error_fail_cmd (); else err = gpg_error (GPG_ERR_UNKNOWN_OPTION); return err; } /* The handler for an Assuan RESET command. */ static gpg_error_t reset_notify (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); (void)line; xfree (ctrl->server_local->devicename); ctrl->server_local->devicename = NULL; es_fclose (ctrl->server_local->devicefp); ctrl->server_local->devicefp = NULL; ctrl->devti = NULL; assuan_close_input_fd (ctx); assuan_close_output_fd (ctx); return 0; } static const char hlp_finddevice[] = "FINDDEVICE \n" "\n" "Find the device matching NAME. NAME be any identifier from\n" "g13tab permissible for the user. The corresponding block\n" "device is returned using a status line."; static gpg_error_t cmd_finddevice (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; tab_item_t ti; const char *s; const char *name; name = skip_options (line); /* Are we allowed to use the given device? We check several names: * 1. The full block device * 2. The label * 3. The final part of the block device if NAME does not have a slash. * 4. The mountpoint */ for (ti=ctrl->client.tab; ti; ti = ti->next) if (!strcmp (name, ti->blockdev)) break; if (!ti) { for (ti=ctrl->client.tab; ti; ti = ti->next) if (ti->label && !strcmp (name, ti->label)) break; } if (!ti && !strchr (name, '/')) { for (ti=ctrl->client.tab; ti; ti = ti->next) { s = strrchr (ti->blockdev, '/'); if (s && s[1] && !strcmp (name, s+1)) break; } } if (!ti) { for (ti=ctrl->client.tab; ti; ti = ti->next) if (ti->mountpoint && !strcmp (name, ti->mountpoint)) break; } if (!ti) { err = set_error (GPG_ERR_NOT_FOUND, "device not configured for user"); goto leave; } /* Check whether we have permissions to open the device. */ { estream_t fp = es_fopen (ti->blockdev, "rb"); if (!fp) { err = gpg_error_from_syserror (); log_error ("error opening '%s': %s\n", ti->blockdev, gpg_strerror (err)); goto leave; } es_fclose (fp); } err = g13_status (ctrl, STATUS_BLOCKDEV, ti->blockdev, NULL); if (err) return err; leave: return leave_cmd (ctx, err); } static const char hlp_device[] = "DEVICE \n" "\n" "Set the device used by further commands.\n" "A device name or a PARTUUID string may be used.\n" "Access to that device (by the g13 system) is locked\n" "until a new DEVICE command or end of this process\n"; static gpg_error_t cmd_device (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; tab_item_t ti; estream_t fp = NULL; line = skip_options (line); /* # warning hardwired to /dev/sdb1 ! */ /* if (strcmp (line, "/dev/sdb1")) */ /* { */ /* err = gpg_error (GPG_ERR_ENOENT); */ /* goto leave; */ /* } */ /* Always close an open device stream of this session. */ xfree (ctrl->server_local->devicename); ctrl->server_local->devicename = NULL; es_fclose (ctrl->server_local->devicefp); ctrl->server_local->devicefp = NULL; /* Are we allowed to use the given device? */ for (ti=ctrl->client.tab; ti; ti = ti->next) if (!strcmp (line, ti->blockdev)) break; if (!ti) { err = set_error (GPG_ERR_EACCES, "device not configured for user"); goto leave; } ctrl->server_local->devicename = xtrystrdup (line); if (!ctrl->server_local->devicename) { err = gpg_error_from_syserror (); goto leave; } /* Check whether we have permissions to open the device and keep an FD open. */ fp = es_fopen (ctrl->server_local->devicename, "rb"); if (!fp) { err = gpg_error_from_syserror (); log_error ("error opening '%s': %s\n", ctrl->server_local->devicename, gpg_strerror (err)); goto leave; } es_fclose (ctrl->server_local->devicefp); ctrl->server_local->devicefp = fp; fp = NULL; ctrl->devti = ti; /* Fixme: Take some kind of lock. */ leave: es_fclose (fp); if (err) { xfree (ctrl->server_local->devicename); ctrl->server_local->devicename = NULL; ctrl->devti = NULL; } return leave_cmd (ctx, err); } static const char hlp_create[] = "CREATE \n" "\n" "Create a new encrypted partition on the current device.\n" " must be \"dm-crypt\" for now."; static gpg_error_t cmd_create (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; estream_t fp = NULL; line = skip_options (line); if (strcmp (line, "dm-crypt")) { err = set_error (GPG_ERR_INV_ARG, "Type must be \"dm-crypt\""); goto leave; } if (!ctrl->server_local->devicename || !ctrl->server_local->devicefp || !ctrl->devti) { err = set_error (GPG_ERR_ENOENT, "No device has been set"); goto leave; } err = sh_is_empty_partition (ctrl->server_local->devicename); if (err) { if (gpg_err_code (err) == GPG_ERR_FALSE) err = gpg_error (GPG_ERR_CONFLICT); err = assuan_set_error (ctx, err, "Partition is not empty"); goto leave; } /* We need a writeable stream to create the container. */ fp = es_fopen (ctrl->server_local->devicename, "r+b"); if (!fp) { err = gpg_error_from_syserror (); log_error ("error opening '%s': %s\n", ctrl->server_local->devicename, gpg_strerror (err)); goto leave; } if (es_setvbuf (fp, NULL, _IONBF, 0)) { err = gpg_error_from_syserror (); log_error ("error setting '%s' to _IONBF: %s\n", ctrl->server_local->devicename, gpg_strerror (err)); goto leave; } err = sh_dmcrypt_create_container (ctrl, ctrl->server_local->devicename, fp); if (es_fclose (fp)) { gpg_error_t err2 = gpg_error_from_syserror (); log_error ("error closing '%s': %s\n", ctrl->server_local->devicename, gpg_strerror (err2)); if (!err) err = err2; } fp = NULL; leave: es_fclose (fp); return leave_cmd (ctx, err); } static const char hlp_getkeyblob[] = "GETKEYBLOB\n" "\n" "Return the encrypted keyblob of the current device."; static gpg_error_t cmd_getkeyblob (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; void *enckeyblob = NULL; size_t enckeybloblen; line = skip_options (line); if (!ctrl->server_local->devicename || !ctrl->server_local->devicefp || !ctrl->devti) { err = set_error (GPG_ERR_ENOENT, "No device has been set"); goto leave; } err = sh_is_empty_partition (ctrl->server_local->devicename); if (!err) { err = gpg_error (GPG_ERR_ENODEV); assuan_set_error (ctx, err, "Partition is empty"); goto leave; } err = 0; err = g13_keyblob_read (ctrl->server_local->devicename, &enckeyblob, &enckeybloblen); if (err) goto leave; err = assuan_send_data (ctx, enckeyblob, enckeybloblen); if (!err) err = assuan_send_data (ctx, NULL, 0); /* Flush */ leave: xfree (enckeyblob); return leave_cmd (ctx, err); } static const char hlp_mount[] = "MOUNT \n" "\n" "Mount an encrypted partition on the current device.\n" " must be \"dm-crypt\" for now."; static gpg_error_t cmd_mount (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; unsigned char *keyblob = NULL; size_t keybloblen; tupledesc_t tuples = NULL; line = skip_options (line); if (strcmp (line, "dm-crypt")) { err = set_error (GPG_ERR_INV_ARG, "Type must be \"dm-crypt\""); goto leave; } if (!ctrl->server_local->devicename || !ctrl->server_local->devicefp || !ctrl->devti) { err = set_error (GPG_ERR_ENOENT, "No device has been set"); goto leave; } err = sh_is_empty_partition (ctrl->server_local->devicename); if (!err) { err = gpg_error (GPG_ERR_ENODEV); assuan_set_error (ctx, err, "Partition is empty"); goto leave; } err = 0; /* We expect that the client already decrypted the keyblob. * Eventually we should move reading of the keyblob to here and ask * the client to decrypt it. */ assuan_begin_confidential (ctx); err = assuan_inquire (ctx, "KEYBLOB", &keyblob, &keybloblen, 4 * 1024); assuan_end_confidential (ctx); if (err) { log_error (_("assuan_inquire failed: %s\n"), gpg_strerror (err)); goto leave; } err = create_tupledesc (&tuples, keyblob, keybloblen); if (!err) keyblob = NULL; else { if (gpg_err_code (err) == GPG_ERR_NOT_SUPPORTED) log_error ("unknown keyblob version received\n"); goto leave; } err = sh_dmcrypt_mount_container (ctrl, ctrl->server_local->devicename, tuples); leave: destroy_tupledesc (tuples); return leave_cmd (ctx, err); } static const char hlp_umount[] = "UMOUNT \n" "\n" "Unmount an encrypted partition and wipe the key.\n" " must be \"dm-crypt\" for now."; static gpg_error_t cmd_umount (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; line = skip_options (line); if (strcmp (line, "dm-crypt")) { err = set_error (GPG_ERR_INV_ARG, "Type must be \"dm-crypt\""); goto leave; } if (!ctrl->server_local->devicename || !ctrl->server_local->devicefp || !ctrl->devti) { err = set_error (GPG_ERR_ENOENT, "No device has been set"); goto leave; } err = sh_dmcrypt_umount_container (ctrl, ctrl->server_local->devicename); leave: return leave_cmd (ctx, err); } static const char hlp_suspend[] = "SUSPEND \n" "\n" "Suspend an encrypted partition and wipe the key.\n" " must be \"dm-crypt\" for now."; static gpg_error_t cmd_suspend (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; line = skip_options (line); if (strcmp (line, "dm-crypt")) { err = set_error (GPG_ERR_INV_ARG, "Type must be \"dm-crypt\""); goto leave; } if (!ctrl->server_local->devicename || !ctrl->server_local->devicefp || !ctrl->devti) { err = set_error (GPG_ERR_ENOENT, "No device has been set"); goto leave; } err = sh_is_empty_partition (ctrl->server_local->devicename); if (!err) { err = gpg_error (GPG_ERR_ENODEV); assuan_set_error (ctx, err, "Partition is empty"); goto leave; } err = 0; err = sh_dmcrypt_suspend_container (ctrl, ctrl->server_local->devicename); leave: return leave_cmd (ctx, err); } static const char hlp_resume[] = "RESUME \n" "\n" "Resume an encrypted partition and set the key.\n" " must be \"dm-crypt\" for now."; static gpg_error_t cmd_resume (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; unsigned char *keyblob = NULL; size_t keybloblen; tupledesc_t tuples = NULL; line = skip_options (line); if (strcmp (line, "dm-crypt")) { err = set_error (GPG_ERR_INV_ARG, "Type must be \"dm-crypt\""); goto leave; } if (!ctrl->server_local->devicename || !ctrl->server_local->devicefp || !ctrl->devti) { err = set_error (GPG_ERR_ENOENT, "No device has been set"); goto leave; } err = sh_is_empty_partition (ctrl->server_local->devicename); if (!err) { err = gpg_error (GPG_ERR_ENODEV); assuan_set_error (ctx, err, "Partition is empty"); goto leave; } err = 0; /* We expect that the client already decrypted the keyblob. * Eventually we should move reading of the keyblob to here and ask * the client to decrypt it. */ assuan_begin_confidential (ctx); err = assuan_inquire (ctx, "KEYBLOB", &keyblob, &keybloblen, 4 * 1024); assuan_end_confidential (ctx); if (err) { log_error (_("assuan_inquire failed: %s\n"), gpg_strerror (err)); goto leave; } err = create_tupledesc (&tuples, keyblob, keybloblen); if (!err) keyblob = NULL; else { if (gpg_err_code (err) == GPG_ERR_NOT_SUPPORTED) log_error ("unknown keyblob version received\n"); goto leave; } err = sh_dmcrypt_resume_container (ctrl, ctrl->server_local->devicename, tuples); leave: destroy_tupledesc (tuples); return leave_cmd (ctx, err); } static const char hlp_getinfo[] = "GETINFO \n" "\n" "Multipurpose function to return a variety of information.\n" "Supported values for WHAT are:\n" "\n" " version - Return the version of the program.\n" " pid - Return the process id of the server.\n" " showtab - Show the table for the user."; static gpg_error_t cmd_getinfo (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; char *buf; if (!strcmp (line, "version")) { const char *s = PACKAGE_VERSION; err = assuan_send_data (ctx, s, strlen (s)); } else if (!strcmp (line, "pid")) { char numbuf[50]; snprintf (numbuf, sizeof numbuf, "%lu", (unsigned long)getpid ()); err = assuan_send_data (ctx, numbuf, strlen (numbuf)); } else if (!strncmp (line, "getsz", 5)) { unsigned long long nblocks; err = sh_blockdev_getsz (line+6, &nblocks); if (!err) log_debug ("getsz=%llu\n", nblocks); } else if (!strcmp (line, "showtab")) { tab_item_t ti; for (ti=ctrl->client.tab; !err && ti; ti = ti->next) { buf = es_bsprintf ("%s %s%s %s %s%s\n", ctrl->client.uname, *ti->blockdev=='/'? "":"partuuid=", ti->blockdev, ti->label? ti->label : "-", ti->mountpoint? " ":"", ti->mountpoint? ti->mountpoint:""); if (!buf) err = gpg_error_from_syserror (); else { err = assuan_send_data (ctx, buf, strlen (buf)); if (!err) err = assuan_send_data (ctx, NULL, 0); /* Flush */ } xfree (buf); } } else err = set_error (GPG_ERR_ASS_PARAMETER, "unknown value for WHAT"); return leave_cmd (ctx, err); } /* This command handler is used for all commands if this process has not been started as expected. */ static gpg_error_t fail_command (assuan_context_t ctx, char *line) { gpg_error_t err; const char *name = assuan_get_command_name (ctx); (void)line; if (!name) name = "?"; err = set_error_fail_cmd (); log_error ("command '%s' failed: %s\n", name, gpg_strerror (err)); return err; } /* Tell the Assuan library about our commands. */ static int register_commands (assuan_context_t ctx, int fail_all) { static struct { const char *name; assuan_handler_t handler; const char * const help; } table[] = { { "FINDDEVICE", cmd_finddevice, hlp_finddevice }, { "DEVICE", cmd_device, hlp_device }, { "CREATE", cmd_create, hlp_create }, { "GETKEYBLOB", cmd_getkeyblob, hlp_getkeyblob }, { "MOUNT", cmd_mount, hlp_mount }, { "UMOUNT", cmd_umount, hlp_umount }, { "SUSPEND", cmd_suspend,hlp_suspend}, { "RESUME", cmd_resume, hlp_resume }, { "INPUT", NULL }, { "OUTPUT", NULL }, { "GETINFO", cmd_getinfo, hlp_getinfo }, { NULL } }; gpg_error_t err; int i; for (i=0; table[i].name; i++) { err = assuan_register_command (ctx, table[i].name, fail_all ? fail_command : table[i].handler, table[i].help); if (err) return err; } return 0; } /* Startup the server. */ gpg_error_t syshelp_server (ctrl_t ctrl) { gpg_error_t err; assuan_fd_t filedes[2]; assuan_context_t ctx = NULL; /* We use a pipe based server so that we can work from scripts. assuan_init_pipe_server will automagically detect when we are called with a socketpair and ignore FILEDES in this case. */ filedes[0] = assuan_fdopen (0); filedes[1] = assuan_fdopen (1); err = assuan_new (&ctx); if (err) { log_error ("failed to allocate an Assuan context: %s\n", gpg_strerror (err)); goto leave; } err = assuan_init_pipe_server (ctx, filedes); if (err) { log_error ("failed to initialize the server: %s\n", gpg_strerror (err)); goto leave; } err = register_commands (ctx, 0 /*FIXME:ctrl->fail_all_cmds*/); if (err) { log_error ("failed to the register commands with Assuan: %s\n", gpg_strerror (err)); goto leave; } assuan_set_pointer (ctx, ctrl); { char *tmp = xtryasprintf ("G13-syshelp %s ready to serve requests " "from %lu(%s)", PACKAGE_VERSION, (unsigned long)ctrl->client.uid, ctrl->client.uname); if (tmp) { assuan_set_hello_line (ctx, tmp); xfree (tmp); } } assuan_register_reset_notify (ctx, reset_notify); assuan_register_option_handler (ctx, option_handler); ctrl->server_local = xtrycalloc (1, sizeof *ctrl->server_local); if (!ctrl->server_local) { err = gpg_error_from_syserror (); goto leave; } ctrl->server_local->assuan_ctx = ctx; while ( !(err = assuan_accept (ctx)) ) { err = assuan_process (ctx); if (err) log_info ("Assuan processing failed: %s\n", gpg_strerror (err)); } if (err == -1) err = 0; else log_info ("Assuan accept problem: %s\n", gpg_strerror (err)); leave: reset_notify (ctx, NULL); /* Release all items hold by SERVER_LOCAL. */ if (ctrl->server_local) { xfree (ctrl->server_local); ctrl->server_local = NULL; } assuan_release (ctx); return err; } gpg_error_t sh_encrypt_keyblob (ctrl_t ctrl, const void *keyblob, size_t keybloblen, char **r_enckeyblob, size_t *r_enckeybloblen) { assuan_context_t ctx = ctrl->server_local->assuan_ctx; gpg_error_t err; unsigned char *enckeyblob; size_t enckeybloblen; *r_enckeyblob = NULL; /* Send the plaintext. */ err = g13_status (ctrl, STATUS_PLAINTEXT_FOLLOWS, NULL); if (err) return err; assuan_begin_confidential (ctx); err = assuan_send_data (ctx, keyblob, keybloblen); if (!err) err = assuan_send_data (ctx, NULL, 0); assuan_end_confidential (ctx); if (!err) err = assuan_write_line (ctx, "END"); if (err) { log_error (_("error sending data: %s\n"), gpg_strerror (err)); return err; } /* Inquire the ciphertext. */ err = assuan_inquire (ctx, "ENCKEYBLOB", &enckeyblob, &enckeybloblen, 16 * 1024); if (err) { log_error (_("assuan_inquire failed: %s\n"), gpg_strerror (err)); return err; } *r_enckeyblob = enckeyblob; *r_enckeybloblen = enckeybloblen; return 0; } /* Send a status line with status ID NO. The arguments are a list of strings terminated by a NULL argument. */ gpg_error_t g13_status (ctrl_t ctrl, int no, ...) { gpg_error_t err; va_list arg_ptr; va_start (arg_ptr, no); err = vprint_assuan_status_strings (ctrl->server_local->assuan_ctx, get_status_string (no), arg_ptr); va_end (arg_ptr); return err; }