From a407afcdaf3b838c04cb49ab9fc21205c1cfef95 Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 23 Dec 2017 21:46:51 -0500 Subject: [PATCH] Add support for pinentry protocol This feature is now used by Emacs so that Emacs doesn't need to handle the passphrase itself. --- README.md | 8 ++++ config.h | 8 ++++ enchive-mode.el | 11 ++--- enchive.1 | 6 ++- src/docs.h | 6 +++ src/enchive.c | 112 +++++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 142 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0b68c81..b1e41b4 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,14 @@ time with an optional argument to `--agent`. Whether or not to enable the agent by default. This can be explicitly overridden at run time with `--agent` and `--no-agent`. +#### `ENCHIVE_PINENTRY_DEFAULT` + +The default program to use for `pinentry`. + +#### `ENCHIVE_PINENTRY_DEFAULT_ENABLED` + +Whether or not to use `pinentry` by default when reading passphrases. + #### `ENCHIVE_KEY_DERIVE_ITERATIONS` Power-of-two exponent for protection key derivation. Can be configured diff --git a/config.h b/config.h index 7eaac1e..9f6781c 100644 --- a/config.h +++ b/config.h @@ -35,6 +35,14 @@ # define ENCHIVE_AGENT_DEFAULT_ENABLED 0 #endif +#ifndef ENCHIVE_PINENTRY_DEFAULT +# define ENCHIVE_PINENTRY_DEFAULT pinentry +#endif + +#ifndef ENCHIVE_PINENTRY_DEFAULT_ENABLED +# define ENCHIVE_PINENTRY_DEFAULT_ENABLED 0 +#endif + #ifndef ENCHIVE_PASSPHRASE_MAX # define ENCHIVE_PASSPHRASE_MAX 1024 #endif diff --git a/enchive-mode.el b/enchive-mode.el index 428fce8..f6f1272 100644 --- a/enchive-mode.el +++ b/enchive-mode.el @@ -4,12 +4,8 @@ ;;; Commentary: -;; Load this file, then M-x `enchive-mode' to enable automatic -;; encryption and decryption of Enchive files. - -;; The agent *must* be started before any encrypted files are opened -;; because this mode doesn't (yet) know how to prompt for a -;; passphrase. +;; Load this file, then M-x `enchive-mode' (global minor mode) to +;; enable automatic encryption and decryption of Enchive files. ;;; Code: @@ -28,7 +24,8 @@ (let ((file-name-handler-alist ())) (cond ((eq operation 'insert-file-contents) (let ((file (car args))) - (unless (= 0 (call-process "enchive" file t "--agent" "extract")) + (unless (= 0 (call-process "enchive" file t nil + "--pinentry" "extract")) (error "Enchive subprocess failed")) (setf buffer-file-name file) (list file (buffer-size)))) diff --git a/enchive.1 b/enchive.1 index b6d6628..c52b907 100644 --- a/enchive.1 +++ b/enchive.1 @@ -40,7 +40,7 @@ Like GnuPG, you can safely encrypt files on systems that you don't trust with yo Files are secured with ChaCha20, Curve25519, and HMAC-SHA256. .SH OPTIONS .TP -\fB\-a\fR \fIseconds\fR, \fB\-\-agent\fR[=\fIseconds\fR] +\fB\-a\fR\fIseconds\fR, \fB\-\-agent\fR[=\fIseconds\fR] Runs the key agent for awhile after successfully reading the passphrase. The agent will remain resident in memory until a period of inactivity passes. Default is 900 seconds (15 minutes). @@ -48,6 +48,10 @@ Default is 900 seconds (15 minutes). \fB\-A\fB, \fB\-\-no\-agent\fR Do not start the key agent (default). .TP +\fB\-e\fR\fIprogram\fR, \fB\-\-pinentry\fR[=\fIprogram\fR] +Read passphrases using the system's pinentry program. By default +Enchive uses the program named "pinentry". +.TP \fB\-p, \-\-pubkey\fR \fIfile\fR Specifies the public key file to use for encryption. .TP diff --git a/src/docs.h b/src/docs.h index ec7f608..24b33e3 100644 --- a/src/docs.h +++ b/src/docs.h @@ -24,6 +24,12 @@ static const char *docs_usage[] = { " (default)", # endif #endif +" -e, --pinentry[=program] use pinentry to read passphrases" +#if ENCHIVE_PINENTRY_DEFAULT_ENABLED + " (default)", +#else + "", +#endif " --version display version information", " --help display this usage information", "", diff --git a/src/enchive.c b/src/enchive.c index 159e756..61d29fe 100644 --- a/src/enchive.c +++ b/src/enchive.c @@ -22,6 +22,12 @@ static int global_agent_timeout = ENCHIVE_AGENT_TIMEOUT; static int global_agent_timeout = 0; #endif +#if ENCHIVE_PINENTRY_DEFAULT_ENABLED +static char *pinentry_path = STR(ENCHIVE_PINENTRY_DEFAULT); +#else +static char *pinentry_path = 0; +#endif + static const char enchive_suffix[] = ".enchive"; static struct { @@ -432,10 +438,107 @@ get_passphrase_dumb(char *buf, size_t len, char *prompt) #include #include +static void +pinentry_decode(char *buf, size_t blen, const char *str) +{ + size_t i, j; + + for (i = 0, j = 0; str[i] && str[i] != '\n' && j < blen - 1; i++) { + int c = str[i]; + if (c == '%') { + static const char *hex = "0123456789ABCDEF"; + char *nibh, *nibl; + if (!str[i + 1] || !str[i + 2]) + fatal("invalid data from pinentry"); + nibh = memchr(hex, str[i + 1], 16); + nibl = memchr(hex, str[i + 2], 16); + if (!nibh || !nibl) + fatal("invalid data from pinentry"); + buf[j++] = (nibh - hex) * 16 + (nibl - hex); + i += 2; + } else { + buf[j++] = c; + } + } + buf[j] = 0; +} + +static void +invoke_pinentry(char *buf, size_t len, char *prompt) +{ + int pin[2]; + int pout[2]; + pid_t pid; + + if (pipe(pin) != 0) + fatal("could not start pinentry -- %s", strerror(errno)); + if (pipe(pout) != 0) + fatal("could not start pinentry -- %s", strerror(errno)); + + pid = fork(); + if (pid == -1) + fatal("pinentry fork() failed -- %s", strerror(errno)); + if (pid) { + FILE *pfi, *pfo; + char line[ENCHIVE_PASSPHRASE_MAX * 3 + 32]; + + close(pin[0]); + close(pout[1]); + + if (!(pfi = fdopen(pin[1], "w"))) + fatal("fdopen() input -- %s", strerror(errno)); + if (!(pfo = fdopen(pout[0], "r"))) + fatal("fdopen() output -- %s", strerror(errno)); + + if (!fgets(line, sizeof(line), pfo)) + /* Likely caused by exec() failure, so exit quietly. */ + exit(EXIT_FAILURE); + if (strncmp(line, "OK", 2) != 0) + fatal("pinentry startup failure"); + + if (fprintf(pfi, "SETPROMPT %s\n", prompt) < 0 || fflush(pfi) < 0) + fatal("pinentry write() -- %s", strerror(errno)); + + if (!fgets(line, sizeof(line), pfo)) + fatal("pinentry read() -- %s", strerror(errno)); + if (strncmp(line, "OK", 2) != 0) + fatal("pinentry protocol failure"); + + if (fprintf(pfi, "GETPIN\n") < 0 || fflush(pfi) < 0) + fatal("pinentry write() -- %s", strerror(errno)); + + if (!fgets(line, sizeof(line), pfo)) + fatal("pinentry read() -- %s", strerror(errno)); + if (strncmp(line, "ERR ", 4) == 0) + fatal("passphrase entry canceled"); + else if (strncmp(line, "OK", 2) == 0) + buf[0] = 0; + else if (strncmp(line, "D ", 2) == 0) + pinentry_decode(buf, len, line + 2); + else + fatal("pinentry protocol failure"); + } else { + close(pin[1]); + close(pout[0]); + dup2(pin[0], STDIN_FILENO); + dup2(pout[1], STDOUT_FILENO); + if (execlp(pinentry_path, pinentry_path, (char *)0)) + fatal("exec(\"%s\") failed -- %s", + pinentry_path, strerror(errno)); + } +} + static void get_passphrase(char *buf, size_t len, char *prompt) { - int tty = open("/dev/tty", O_RDWR); + int tty; + + if (pinentry_path) { + invoke_pinentry(buf, len, prompt); + return; + } + + tty = open("/dev/tty", O_RDWR); if (tty == -1) { get_passphrase_dumb(buf, len, prompt); } else { @@ -1401,6 +1504,7 @@ main(int argc, char **argv) {"agent", 'a', OPTPARSE_OPTIONAL}, {"no-agent", 'A', OPTPARSE_NONE}, #endif + {"pinentry", 'e', OPTPARSE_OPTIONAL}, {"pubkey", 'p', OPTPARSE_REQUIRED}, {"seckey", 's', OPTPARSE_REQUIRED}, {"version", 'V', OPTPARSE_NONE}, @@ -1433,6 +1537,12 @@ main(int argc, char **argv) global_agent_timeout = 0; break; #endif + case 'e': + if (options->optarg) + pinentry_path = options->optarg; + else + pinentry_path = STR(ENCHIVE_PINENTRY_DEFAULT); + break; case 'p': global_pubkey = options->optarg; break;