view mcabber/src/pgp.c @ 1197:6f602d3270a4

Add /pgp [-]force With this command it becomes possible to enforce PGP encryption without checking if the remote client has PGP support. It can be used to send encrypted offline messages too.
author Mikael Berthe <mikael@lilotux.net>
date Fri, 27 Apr 2007 00:37:57 +0200
parents 265e1d873d3e
children 2de8f8ba1f34
line wrap: on
line source

/*
 * pgp.c        -- PGP utility functions
 *
 * Copyright (C) 2006 Mikael Berthe <bmikael@lists.lilotux.net>
 * Some parts inspired by centericq (impgp.cc)
 *
 * This program 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 2 of the License, or (at
 * your option) any later version.
 *
 * This program 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

#include <config.h>

#ifdef HAVE_GPGME

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <locale.h>
#include <sys/mman.h>
#include <glib.h>

#include "pgp.h"
#include "logprint.h"

#define MIN_GPGME_VERSION "1.0.0"

static struct gpg_struct
{
  int enabled;
  char *private_key;
  char *passphrase;
} gpg;


//  gpg_init(priv_key, passphrase)
// Initialize the GPG sub-systems.  This function must be invoked early.
// Note: priv_key & passphrase are optional, they can be set later.
// This function returns 0 if gpgme is available and initialized;
// if not it returns the gpgme error code.
int gpg_init(const char *priv_key, const char *passphrase)
{
  gpgme_error_t err;

  // Check for version and OpenPGP protocol support.
  if (!gpgme_check_version(MIN_GPGME_VERSION)) {
    scr_LogPrint(LPRINT_LOGNORM,
                 "GPGME initialization error: Bad library version");
    return -1;
  }

  err = gpgme_engine_check_version(GPGME_PROTOCOL_OpenPGP);
  if (err) {
    scr_LogPrint(LPRINT_LOGNORM|LPRINT_NOTUTF8,
                 "GPGME initialization error: %s", gpgme_strerror(err));
    return err;
  }

  // Set the locale information.
  gpgme_set_locale(NULL, LC_CTYPE, setlocale(LC_CTYPE, NULL));
  gpgme_set_locale(NULL, LC_MESSAGES, setlocale(LC_MESSAGES, NULL));

  // Store private data.
  gpg_set_private_key(priv_key);
  gpg_set_passphrase(passphrase);

  gpg.enabled = 1;
  return 0;
}

//  gpg_terminate()
// Destroy data and free memory.
void gpg_terminate(void)
{
  gpg.enabled = 0;
  gpg_set_passphrase(NULL);
  gpg_set_private_key(NULL);
}

//  gpg_set_passphrase(passphrase)
// Set the current passphrase (use NULL to erase it).
void gpg_set_passphrase(const char *passphrase)
{
  // Remove current passphrase
  if (gpg.passphrase) {
    ssize_t len = strlen(gpg.passphrase);
    memset(gpg.passphrase, 0, len);
    munlock(gpg.passphrase, len);
    g_free(gpg.passphrase);
  }
  if (passphrase) {
    gpg.passphrase = g_strdup(passphrase);
    mlock(gpg.passphrase, strlen(gpg.passphrase));
  } else {
    gpg.passphrase = NULL;
  }
}

//  gpg_set_private_key(keyid)
// Set the current private key id (use NULL to unset it).
void gpg_set_private_key(const char *priv_keyid)
{
  g_free(gpg.private_key);
  if (priv_keyid)
    gpg.private_key = g_strdup(priv_keyid);
  else
    gpg.private_key = NULL;
}

//  strip_header_footer(data)
// Remove PGP header & footer from data.
// Return a new string, or NULL.
// The string must be freed by the caller with g_free() when no longer needed.
static char *strip_header_footer(const char *data)
{
  char *p, *q;

  if (!data)
    return NULL;

  // p: beginning of real data
  // q: end of real data

  // Strip header (to the first empty line)
  p = strstr(data, "\n\n");
  if (!p)
    return g_strdup(data);

  // Strip footer
  // We want to remove the last lines, until the line beginning with a '-'
  p += 2;
  for (q = p ; *q; q++) ;
  // (q is at the end of data now)
  for (q--; q > p && (*q != '\n' || *(q+1) != '-'); q--) ;

  if (q <= p)
    return NULL; // Shouldn't happen...

  return g_strndup(p, q-p);
}

//  passphrase_cb()
// GPGME passphrase callback function.
static gpgme_error_t passphrase_cb(void *hook, const char *uid_hint,
                       const char *passphrase_info, int prev_was_bad, int fd)
{
  ssize_t len;

  // Abort if we do not have the password.
  if (!gpg.passphrase) {
    write(fd, "\n", 1);
    return gpg_error(GPG_ERR_CANCELED);
  }

  // Write the passphrase to the file descriptor.
  len = strlen(gpg.passphrase);
  if (write(fd, gpg.passphrase, len) != len)
    return gpg_error(GPG_ERR_CANCELED);
  if (write(fd, "\n", 1) != 1)
    return gpg_error(GPG_ERR_CANCELED);

  return 0; // Success
}

//  gpg_verify(gpg_data, text, *sigsum)
// Verify that gpg_data is a correct signature for text.
// Return the key id (or fingerprint), and set *sigsum to
// the gpgme signature summary value.
// The returned string must be freed with g_free() after use.
char *gpg_verify(const char *gpg_data, const char *text,
                 gpgme_sigsum_t *sigsum)
{
  gpgme_ctx_t ctx;
  gpgme_data_t data_sign, data_text;
  char *data;
  char *verified_key = NULL;
  gpgme_key_t key;
  gpgme_error_t err;
  const char prefix[] = "-----BEGIN PGP SIGNATURE-----\n\n";
  const char suffix[] = "\n-----END PGP SIGNATURE-----\n";

  // Reset the summary.
  *sigsum = 0;

  if (!gpg.enabled)
    return NULL;

  err = gpgme_new(&ctx);
  if (err) {
    scr_LogPrint(LPRINT_LOGNORM|LPRINT_NOTUTF8,
                 "GPGME error: %s", gpgme_strerror(err));
    return NULL;
  }

  gpgme_set_protocol(ctx, GPGME_PROTOCOL_OpenPGP);

  // Surround the given data with the prefix & suffix
  data = g_new(char, sizeof(prefix) + sizeof(suffix) + strlen(gpg_data));
  strcpy(data, prefix);
  strcat(data, gpg_data);
  strcat(data, suffix);

  err = gpgme_data_new_from_mem(&data_sign, data, strlen(data), 0);
  if (!err) {
    err = gpgme_data_new_from_mem(&data_text, text, strlen(text), 0);
    if (!err) {
      err = gpgme_op_verify(ctx, data_sign, data_text, 0);
      if (!err) {
        gpgme_verify_result_t vr = gpgme_op_verify_result(ctx);
        if (vr && vr->signatures) {
          char *r = vr->signatures->fpr;
          // Found the fingerprint.  Let's try to get the key id.
          if (!gpgme_get_key(ctx, r, &key, 0) && key) {
            r = key->subkeys->keyid;
            gpgme_key_release(key);
          }
          // r is a static variable, let's copy it.
          verified_key = g_strdup(r);
          *sigsum = vr->signatures->summary;
          // For some reason summary could be 0 when status is 0 too,
          // which means the signature is valid...
          if (!*sigsum && !vr->signatures->status)
            *sigsum = GPGME_SIGSUM_GREEN;
        }
      }
      gpgme_data_release(data_text);
    }
    gpgme_data_release(data_sign);
  }
  if (err)
    scr_LogPrint(LPRINT_LOGNORM|LPRINT_NOTUTF8,
                 "GPGME verification error: %s", gpgme_strerror(err));
  gpgme_release(ctx);
  g_free(data);
  return verified_key;
}

//  gpg_sign(gpg_data)
// Return a signature of gpg_data (or NULL).
// The returned string must be freed with g_free() after use.
char *gpg_sign(const char *gpg_data)
{
  gpgme_ctx_t ctx;
  gpgme_data_t in, out;
  char *p;
  char *signed_data = NULL;
  size_t nread;
  gpgme_key_t key;
  gpgme_error_t err;

  if (!gpg.enabled || !gpg.private_key)
    return NULL;

  err = gpgme_new(&ctx);
  if (err) {
    scr_LogPrint(LPRINT_LOGNORM|LPRINT_NOTUTF8,
                 "GPGME error: %s", gpgme_strerror(err));
    return NULL;
  }

  gpgme_set_protocol(ctx, GPGME_PROTOCOL_OpenPGP);
  gpgme_set_textmode(ctx, 0);
  gpgme_set_armor(ctx, 1);

  p = getenv("GPG_AGENT_INFO");
  if (!(p && strchr(p, ':')))
    gpgme_set_passphrase_cb(ctx, passphrase_cb, 0);

  err = gpgme_get_key(ctx, gpg.private_key, &key, 1);
  if (err || !key) {
    scr_LogPrint(LPRINT_LOGNORM, "GPGME error: private key not found");
    gpgme_release(ctx);
    return NULL;
  }

  gpgme_signers_clear(ctx);
  gpgme_signers_add(ctx, key);
  gpgme_key_release(key);
  err = gpgme_data_new_from_mem(&in, gpg_data, strlen(gpg_data), 0);
  if (!err) {
    err = gpgme_data_new(&out);
    if (!err) {
      err = gpgme_op_sign(ctx, in, out, GPGME_SIG_MODE_DETACH);
      if (!err) {
        signed_data = gpgme_data_release_and_get_mem(out, &nread);
        if (signed_data) {
          // We need to add a trailing NULL
          char *dd = g_strndup(signed_data, nread);
          free(signed_data);
          signed_data = strip_header_footer(dd);
          g_free(dd);
        }
      } else {
        gpgme_data_release(out);
      }
    }
    gpgme_data_release(in);
  }
  if (err && err != GPG_ERR_CANCELED)
    scr_LogPrint(LPRINT_LOGNORM|LPRINT_NOTUTF8,
                 "GPGME signature error: %s", gpgme_strerror(err));
  gpgme_release(ctx);
  return signed_data;
}

//  gpg_decrypt(gpg_data)
// Return decrypted gpg_data (or NULL).
// The returned string must be freed with g_free() after use.
char *gpg_decrypt(const char *gpg_data)
{
  gpgme_ctx_t ctx;
  gpgme_data_t in, out;
  char *p, *data;
  char *decrypted_data = NULL;
  size_t nread;
  gpgme_error_t err;
  const char prefix[] = "-----BEGIN PGP MESSAGE-----\n\n";
  const char suffix[] = "\n-----END PGP MESSAGE-----\n";

  if (!gpg.enabled)
    return NULL;

  err = gpgme_new(&ctx);
  if (err) {
    scr_LogPrint(LPRINT_LOGNORM|LPRINT_NOTUTF8,
                 "GPGME error: %s", gpgme_strerror(err));
    return NULL;
  }

  gpgme_set_protocol(ctx, GPGME_PROTOCOL_OpenPGP);

  p = getenv("GPG_AGENT_INFO");
  if (!(p && strchr(p, ':')))
    gpgme_set_passphrase_cb(ctx, passphrase_cb, 0);

  // Surround the given data with the prefix & suffix
  data = g_new(char, sizeof(prefix) + sizeof(suffix) + strlen(gpg_data));
  strcpy(data, prefix);
  strcat(data, gpg_data);
  strcat(data, suffix);

  err = gpgme_data_new_from_mem(&in, data, strlen(data), 0);
  if (!err) {
    err = gpgme_data_new(&out);
    if (!err) {
      err = gpgme_op_decrypt(ctx, in, out);
      if (!err) {
        decrypted_data = gpgme_data_release_and_get_mem(out, &nread);
        if (decrypted_data) {
          // We need to add a trailing NULL
          char *dd = g_strndup(decrypted_data, nread);
          free(decrypted_data);
          decrypted_data = dd;
        }
      } else {
        gpgme_data_release(out);
      }
    }
    gpgme_data_release(in);
  }
  if (err && err != GPG_ERR_CANCELED)
    scr_LogPrint(LPRINT_LOGNORM|LPRINT_NOTUTF8,
                 "GPGME decryption error: %s", gpgme_strerror(err));
  gpgme_release(ctx);
  g_free(data);
  return decrypted_data;
}

//  gpg_encrypt(gpg_data, keyid)
// Return encrypted gpg_data with the key keyid (or NULL).
// The returned string must be freed with g_free() after use.
char *gpg_encrypt(const char *gpg_data, const char *keyid)
{
  gpgme_ctx_t ctx;
  gpgme_data_t in, out;
  char *encrypted_data = NULL, *edata;
  size_t nread;
  gpgme_key_t key;
  gpgme_error_t err;

  if (!gpg.enabled)
    return NULL;

  err = gpgme_new(&ctx);
  if (err) {
    scr_LogPrint(LPRINT_LOGNORM|LPRINT_NOTUTF8,
                 "GPGME error: %s", gpgme_strerror(err));
    return NULL;
  }

  gpgme_set_protocol(ctx, GPGME_PROTOCOL_OpenPGP);
  gpgme_set_textmode(ctx, 0);
  gpgme_set_armor(ctx, 1);

  err = gpgme_get_key(ctx, keyid, &key, 0);
  if (!err && key) {
    gpgme_key_t keys[] = { key, 0 };
    err = gpgme_data_new_from_mem(&in, gpg_data, strlen(gpg_data), 0);
    if (!err) {
      err = gpgme_data_new(&out);
      if (!err) {
        err = gpgme_op_encrypt(ctx, keys, GPGME_ENCRYPT_ALWAYS_TRUST, in, out);
        if (!err)
          encrypted_data = gpgme_data_release_and_get_mem(out, &nread);
        else
          gpgme_data_release(out);
      }
      gpgme_data_release(in);
    }
    gpgme_key_release(key);
  } else {
    scr_LogPrint(LPRINT_LOGNORM, "GPGME encryption error: key not found");
    err = 0;
  }
  if (err && err != GPG_ERR_CANCELED)
    scr_LogPrint(LPRINT_LOGNORM|LPRINT_NOTUTF8,
                 "GPGME encryption error: %s", gpgme_strerror(err));
  gpgme_release(ctx);
  edata = strip_header_footer(encrypted_data);
  if (encrypted_data)
    free(encrypted_data);
  return edata;
}

//  gpg_test_passphrase()
// Test the current gpg.passphrase with gpg.private_key.
// If the test doesn't succeed, the passphrase is cleared and a non-null
// value is returned.
int gpg_test_passphrase(void)
{
  char *s;

  if (!gpg.private_key)
    return -1; // No private key...

  s = gpg_sign("test");
  if (s) {
    free(s);
    return 0; // Ok, test successful
  }
  // The passphrase is wrong (if provided)
  gpg_set_passphrase(NULL);
  return -1;
}

inline int gpg_enabled(void)
{
  return gpg.enabled;
}

#else  /* not HAVE_GPGME */

inline int gpg_enabled(void)
{
  return 0;
}

#endif /* HAVE_GPGME */

/* vim: set expandtab cindent cinoptions=>2\:2(0:  For Vim users... */