diff --git a/agent/call-pinentry.c b/agent/call-pinentry.c index 6648a18ba..fe1913e86 100644 --- a/agent/call-pinentry.c +++ b/agent/call-pinentry.c @@ -39,6 +39,7 @@ #include #include "../common/sysutils.h" #include "../common/i18n.h" +#include "../common/zb32.h" #ifdef _POSIX_OPEN_MAX #define MAX_OPEN_FDS _POSIX_OPEN_MAX @@ -53,6 +54,11 @@ time. */ #define LOCK_TIMEOUT (1*60) +/* Define the maximum tries to generate a pin for the GENPIN inquire */ +#define MAX_GENPIN_TRIES 10 +/* Define the number of characters to use for a generated pin */ +#define DEFAULT_GENPIN_BYTES (128 / 8) + /* The assuan context of the current pinentry. */ static assuan_context_t entry_ctx; @@ -818,14 +824,41 @@ estimate_passphrase_quality (const char *pw) } -/* Handle the QUALITY inquiry. */ +/* Generate a random passphrase in zBase32 encoding (RFC-6189) to be + * used by pinetry to suggest a passphrase. */ +static char * +generate_pin (void) +{ + size_t nbytes = opt.min_passphrase_len; + void *rand; + char *generated; + + if (nbytes < 8) + { + nbytes = DEFAULT_GENPIN_BYTES; + } + + rand = gcry_random_bytes_secure (nbytes, GCRY_STRONG_RANDOM); + if (!rand) + { + log_error ("failed to generate random pin\n"); + return NULL; + } + + generated = zb32_encode (rand, nbytes * 8); + gcry_free (rand); + return generated; +} + + +/* Handle inquiries. */ static gpg_error_t -inq_quality (void *opaque, const char *line) +inq_cb (void *opaque, const char *line) { assuan_context_t ctx = opaque; const char *s; char *pin; - int rc; + gpg_error_t err; int percent; char numbuf[20]; @@ -833,24 +866,147 @@ inq_quality (void *opaque, const char *line) { pin = unescape_passphrase_string (s); if (!pin) - rc = gpg_error_from_syserror (); + err = gpg_error_from_syserror (); else { percent = estimate_passphrase_quality (pin); if (check_passphrase_constraints (NULL, pin, 0, NULL)) percent = -percent; snprintf (numbuf, sizeof numbuf, "%d", percent); - rc = assuan_send_data (ctx, numbuf, strlen (numbuf)); + err = assuan_send_data (ctx, numbuf, strlen (numbuf)); xfree (pin); } } + else if ((s = has_leading_keyword (line, "CHECKPIN"))) + { + char *errtext = NULL; + size_t errtextlen; + + if (!opt.enforce_passphrase_constraints) + { + log_error ("unexpected inquiry 'CHECKPIN' without enforced " + "passphrase constraints\n"); + err = gpg_error (GPG_ERR_ASS_UNEXPECTED_CMD); + goto leave; + } + + pin = unescape_passphrase_string (s); + if (!pin) + err = gpg_error_from_syserror (); + else + { + if (check_passphrase_constraints (NULL, pin, 0, &errtext)) + { + if (errtext) + { + /* Unescape the percent-escaped errtext because + assuan_send_data escapes it again. */ + errtextlen = percent_unescape_inplace (errtext, 0); + err = assuan_send_data (ctx, errtext, errtextlen); + } + else + { + log_error ("passphrase check failed without error text\n"); + err = gpg_error (GPG_ERR_GENERAL); + } + } + else + { + err = assuan_send_data (ctx, NULL, 0); + } + xfree (errtext); + xfree (pin); + } + } + else if ((s = has_leading_keyword (line, "GENPIN"))) + { + int tries; + + for (tries = 0; tries < MAX_GENPIN_TRIES; tries ++) + { + pin = generate_pin (); + if (!pin) + { + log_error ("failed to generate a passphrase\n"); + err = gpg_error (GPG_ERR_GENERAL); + goto leave; + } + if (!check_passphrase_constraints (NULL, pin, 0, NULL)) + { + assuan_begin_confidential (ctx); + err = assuan_send_data (ctx, pin, strlen (pin)); + assuan_end_confidential (ctx); + xfree (pin); + goto leave; + } + xfree (pin); + } + log_error ("failed to generate a passphrase after %i tries\n", + MAX_GENPIN_TRIES); + err = gpg_error (GPG_ERR_GENERAL); + } else { log_error ("unsupported inquiry '%s' from pinentry\n", line); - rc = gpg_error (GPG_ERR_ASS_UNKNOWN_INQUIRE); + err = gpg_error (GPG_ERR_ASS_UNKNOWN_INQUIRE); } - return rc; + leave: + return err; +} + + +/* Helper to setup pinentry for genpin action. */ +static gpg_error_t +setup_genpin (ctrl_t ctrl) +{ + gpg_error_t err; + char line[ASSUAN_LINELENGTH]; + char *tmpstr, *tmpstr2; + const char *tooltip; + + (void)ctrl; + + /* TRANSLATORS: This string is displayed by Pinentry as the label + for generating a passphrase. */ + tmpstr = try_percent_escape (L_("Suggest"), "\t\r\n\f\v"); + snprintf (line, DIM(line), "SETGENPIN %s", tmpstr? tmpstr:""); + xfree (tmpstr); + err = assuan_transact (entry_ctx, line, NULL, NULL, NULL, NULL, NULL, NULL); + if (gpg_err_code (err) == 103 /*(Old assuan error code)*/ + || gpg_err_code (err) == GPG_ERR_ASS_UNKNOWN_CMD) + ; /* Ignore Unknown Command from old Pinentry versions. */ + else if (err) + return err; + + tmpstr2 = gnupg_get_help_string ("pinentry.genpin.tooltip", 0); + if (tmpstr2) + tooltip = tmpstr2; + else + { + /* TRANSLATORS: This string is a tooltip, shown by pinentry when + hovering over the generate button. Please use an appropriate + string to describe what this is about. The length of the + tooltip is limited to about 900 characters. If you do not + translate this entry, a default English text (see source) + will be used. The strcmp thingy is there to detect a + non-translated string. */ + tooltip = L_("pinentry.genpin.tooltip"); + if (!strcmp ("pinentry.genpin.tooltip", tooltip)) + tooltip = "Suggest a random passphrase."; + } + tmpstr = try_percent_escape (tooltip, "\t\r\n\f\v"); + xfree (tmpstr2); + snprintf (line, DIM(line), "SETGENPIN_TT %s", tmpstr? tmpstr:""); + xfree (tmpstr); + err = assuan_transact (entry_ctx, line, NULL, NULL, NULL, NULL, NULL, NULL); + if (gpg_err_code (err) == 103 /*(Old assuan error code)*/ + || gpg_err_code (err) == GPG_ERR_ASS_UNKNOWN_CMD) + ; /* Ignore Unknown Command from old pinentry versions. */ + else if (err) + return err; + + return 0; } @@ -904,6 +1060,65 @@ setup_formatted_passphrase (ctrl_t ctrl) } +/* Helper to setup pinentry for enforced passphrase constraints. */ +static gpg_error_t +setup_enforced_constraints (ctrl_t ctrl) +{ + static const struct { const char *key, *help_id, *value; } tbl[] = { + { "hint-short", "pinentry.constraints.hint.short", NULL }, + { "hint-long", "pinentry.constraints.hint.long", NULL }, + /* TRANSLATORS: This is a text shown by pinentry as title of a dialog + telling the user that the entered new passphrase does not satisfy + the passphrase constraints. Please keep it short. */ + { "error-title", NULL, N_("Passphrase Not Allowed") }, + { NULL, NULL } + }; + + gpg_error_t rc; + char line[ASSUAN_LINELENGTH]; + int idx; + char *tmpstr; + const char *s; + char *escapedstr; + + (void)ctrl; + + if (opt.enforce_passphrase_constraints) + { + snprintf (line, DIM(line), "OPTION constraints-enforce"); + rc = assuan_transact (entry_ctx, line, NULL, NULL, NULL, NULL, NULL, + NULL); + if (rc && gpg_err_code (rc) != GPG_ERR_UNKNOWN_OPTION) + return rc; + + for (idx=0; tbl[idx].key; idx++) + { + tmpstr = gnupg_get_help_string (tbl[idx].help_id, 0); + if (tmpstr) + s = tmpstr; + else if (tbl[idx].value) + s = L_(tbl[idx].value); + else + { + log_error ("no help string found for %s\n", tbl[idx].help_id); + continue; + } + escapedstr = try_percent_escape (s, "\t\r\n\f\v"); + xfree (tmpstr); + snprintf (line, DIM(line), "OPTION constraints-%s=%s", + tbl[idx].key, escapedstr); + xfree (escapedstr); + rc = assuan_transact (entry_ctx, line, NULL, NULL, NULL, NULL, NULL, + NULL); + if (rc && gpg_err_code (rc) != GPG_ERR_UNKNOWN_OPTION) + return rc; + } + } + + return 0; +} + + /* Helper for agent_askpin and agent_get_passphrase. */ static gpg_error_t setup_qualitybar (ctrl_t ctrl) @@ -1016,7 +1231,7 @@ do_getpin (ctrl_t ctrl, struct entry_parm_s *parm) assuan_begin_confidential (entry_ctx); rc = assuan_transact (entry_ctx, "GETPIN", getpin_cb, parm, - inq_quality, entry_ctx, + inq_cb, entry_ctx, pinentry_status_cb, &parm->status); assuan_set_flag (entry_ctx, ASSUAN_CONFIDENTIAL, saveflag); /* Most pinentries out in the wild return the old Assuan error code @@ -1415,6 +1630,12 @@ agent_get_passphrase (ctrl_t ctrl, NULL, NULL, NULL, NULL, NULL, NULL); if (rc) pininfo->with_repeat = 0; /* Pinentry does not support it. */ + + (void)setup_genpin (ctrl); + + rc = setup_enforced_constraints (ctrl); + if (rc) + return unlock_pinentry (ctrl, rc); } pininfo->repeat_okay = 0; pininfo->status = 0;