cgnupg

How to connect GnuPG pinentry to a C project


I need an MWE of use GnuPG pinentry with Assuan protocol in C project, i.e. Assuan context proper initialization, connection, etc.

I've tried to read docs, but both pinentry and Assuan documentation are not very helpful on this subject, especially for a beginner.


Solution

  • Well, I've read gpg-agent sources and have finally found some clues. The following code is working well for pinentry-qt5, though it definitely need some security tuning.

    #include <assuan.h>
    #include <gcrypt.h> // secure memory allocation for the passphrase
    #include <string.h>
    
    // This wrapper is for Assuan commands that return OK/<error> and nothing more
    void pinentry_nooutput_cmd (assuan_context_t ctx_p, const char *cmd) {
        char *line;
        size_t linelen;
        gpg_error_t err;
        err = assuan_write_line (ctx_p, cmd);
        if (err) {
            fprintf (stderr, "assuan send error: %s\n", gpg_strerror (err));
            exit (1);
        }
        err = assuan_read_line (ctx_p, &line, &linelen);
        if (err) {
            fprintf (stderr, "assuan receive error: %s\n", gpg_strerror (err));
            exit (1);
        }
        if (strcmp (line, "OK")) {
            fprintf (stderr, "pinentry error: %s\n", line);
            exit (1);
        }
    }
    
    // This function is used to get a passphrase from the user and store it in secmem
    // 'repeat' is 1 for a new passphrase, and 0 otherwise
    char *pinentry_get_passphrase (const char *pinentry_exec, int repeat) {
        assuan_context_t ctx_p;
        gpg_error_t err;
        // pinentry command line options. --no-global-grab is used for debug
        const char *argv[] = { "--no-global-grab", NULL };
        // file descriptors that should stay open. Apparently none is needed
        assuan_fd_t fd_child_list[] = { ASSUAN_INVALID_FD };
        // context allocation
        err = assuan_new (&ctx_p);
        if (err) if (err) {
            fprintf (stderr, "cannot allocate assuan context: %s\n", gpg_strerror (err));
            exit (1);
        }
        // connect to pinentry.
        // here can be a callback for pinentry memory cleanup, but it is not necessary and can be NULL
        err = assuan_pipe_connect (ctx_p, pinentry_exec, argv, fd_child_list, NULL, NULL, 0);
        if (err) {
            fprintf (stderr, "cannot connect to pinentry: %s\n", gpg_strerror (err));
            exit (1);
        }
        // example pinentry tuning according to its docs
        pinentry_nooutput_cmd (ctx_p, "SETTIMEOUT 120");
        pinentry_nooutput_cmd (ctx_p, "SETTITLE Password");
        pinentry_nooutput_cmd (ctx_p, "SETDESC Please enter password:");
        // set repeat if needed
        if (repeat)
            pinentry_nooutput_cmd (ctx_p, "SETREPEAT");
        // get passphrase
        err = assuan_write_line (ctx_p, "GETPIN");
        if (err) {
            fprintf (stderr, "assuan send error: %s\n", gpg_strerror (err));
            exit (1);
        }
        char *line;
        size_t linelen;
        err = assuan_read_line (ctx_p, &line, &linelen);
        if (err) {
            fprintf (stderr, "assuan receive error: %s\n", gpg_strerror (err));
            exit (1);
        }
        // if repeat, check the answer and read one more line
        if (repeat) {
            if (strcmp (line, "S PIN_REPEATED")) {
                fprintf (stderr, "pinentry error: %s\n", line);
                exit (1);
            }
            err = assuan_read_line (ctx_p, &line, &linelen);
            if (err) {
                fprintf (stderr, "assuan receive error: %s\n", gpg_strerror (err));
                exit (1);
            }
        }
        // check for empty password if needed
        if (!strcmp (line, "OK")) {
            fputs ("error: empty password\n", stderr);
            exit (1);
        }
        // read the passphrase (answer is "D <passphrase>")
        if (*line != 'D') {
            fprintf (stderr, "pinentry error: %s\n", line);
            exit (1);
        }
        // move pointer
        line += 2;
        // get password length
        size_t length = strlen (line);
        // alloc secmem
        char *password = gcry_malloc_secure (length + 1);
        memmove (password, line, length);
        // add null termination
        *(line + length) = '\0';
        // release the context
        assuan_release (ctx_p);
        return password;
    }
    
    int main () {
        char *newpass = pinentry_get_passphrase ("/usr/bin/pinentry", 1);
        fprintf (stdout, "New pass: %s\n", newpass);
        gcry_free (newpass);
        char *pass = pinentry_get_passphrase ("/usr/bin/pinentry", 0);
        fprintf (stdout, "Pass: %s\n", pass);
        gcry_free (pass);
        return 0;
    }
    

    Compile:

    gcc -x c -std=c17 \
        -o pinentry_test \
        `pkg-config --cflags libassuan gpg-error libgcrypt` \
        pinentry_test.c \
        `pkg-config --libs libassuan gpg-error libgcrypt`;