You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
566 lines
15 KiB
566 lines
15 KiB
/* |
|
* pgp.c -- PGP utility functions |
|
* |
|
* Copyright (C) 2006-2015 Mikael Berthe <mikael@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, see <http://www.gnu.org/licenses/>. |
|
*/ |
|
|
|
|
|
#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 "settings.h" |
|
#include "utils.h" |
|
#include "logprint.h" |
|
|
|
#define MIN_GPGME_VERSION "1.1.0" |
|
|
|
static struct gpg_struct |
|
{ |
|
int enabled; |
|
int version1; |
|
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; |
|
|
|
gpgme_ctx_t ctx; |
|
gpgme_engine_info_t info; |
|
const char *gpg_path, *gpg_home; |
|
|
|
// 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)); |
|
|
|
// The path to the gpg binary can be specified in order to force |
|
// version 1, for example. |
|
gpg_path = settings_opt_get("gpg_path"); |
|
gpg_home = settings_opt_get("gpg_home"); |
|
if (gpg_path || gpg_home) { |
|
char *xp_gpg_home = expand_filename(gpg_home); |
|
err = gpgme_set_engine_info(GPGME_PROTOCOL_OpenPGP, gpg_path, xp_gpg_home); |
|
g_free(xp_gpg_home); |
|
if (err) return -1; |
|
} |
|
|
|
// Store private data. |
|
gpg_set_private_key(priv_key); |
|
gpg_set_passphrase(passphrase); |
|
|
|
err = gpgme_new(&ctx); |
|
if (err) return -1; |
|
|
|
// Check OpenPGP engine version; with version 2+ the agent is mandatory |
|
// and we do not manage the passphrase. |
|
gpgme_set_protocol(ctx, GPGME_PROTOCOL_OpenPGP); |
|
if (err) return -1; |
|
|
|
err = gpgme_get_engine_info(&info); |
|
if (!err) { |
|
while (info && info->protocol != gpgme_get_protocol(ctx)) |
|
info = info->next; |
|
|
|
if (info && info->version) { |
|
if (!strncmp(info->version, "1.", 2)) |
|
gpg.version1 = TRUE; |
|
scr_log_print(LPRINT_DEBUG, "GPGME: Engine version is '%s'.", |
|
info->version); |
|
} |
|
} |
|
|
|
gpgme_release(ctx); |
|
gpg.enabled = 1; |
|
return 0; |
|
} |
|
|
|
// gpg_is_version1() |
|
// Return TRUE if the GnuPG OpenPGP engine version is 1.x |
|
int gpg_is_version1(void) |
|
{ |
|
return gpg.version1; |
|
} |
|
|
|
// 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; |
|
} |
|
|
|
// gpg_get_private_key_id() |
|
// Return the current private key id (static string). |
|
const char *gpg_get_private_key_id(void) |
|
{ |
|
return gpg.private_key; |
|
} |
|
|
|
// 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); |
|
} |
|
|
|
// GCC ignores casts to void, thus we need to hack around that |
|
static inline void ignore(void*x) {} |
|
|
|
// 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) { |
|
ignore((void*)write(fd, "\n", 1)); // We have an error anyway, thus it does |
|
// not matter if we fail again. |
|
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) { |
|
gpgme_signature_t s = NULL; |
|
// check all signatures and stop if the first could be verified |
|
for (s = vr->signatures; s && !verified_key; s = s->next) { |
|
// Found the fingerprint. Let's try to get the key id. |
|
if (NULL != s->fpr) { |
|
if (!gpgme_get_key(ctx, s->fpr, &key, 0)) { |
|
if (key) { |
|
verified_key = g_strdup(key->subkeys->keyid); |
|
gpgme_key_release(key); |
|
} |
|
} |
|
} |
|
*sigsum = s->summary; |
|
// For some reason summary could be 0 when status is 0 too, |
|
// which means the signature is valid... |
|
if ((!*sigsum) && (!s->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 *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); |
|
|
|
if (gpg.version1) { |
|
// GPG_AGENT_INFO isn't used by GnuPG version 2+ |
|
char *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) { |
|
gpgme_data_release(out); |
|
} else { |
|
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); |
|
} |
|
} |
|
} |
|
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 *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); |
|
|
|
if (gpg.version1) { |
|
// GPG_AGENT_INFO isn't used by GnuPG version 2+ |
|
char *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) { |
|
gpgme_data_release(out); |
|
} else { |
|
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; |
|
} |
|
} |
|
} |
|
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, keyids[], n) |
|
// Return encrypted gpg_data with the n keys from the keyids array (or NULL). |
|
// The returned string must be freed with g_free() after use. |
|
char *gpg_encrypt(const char *gpg_data, const char *keyids[], size_t nkeys) |
|
{ |
|
gpgme_ctx_t ctx; |
|
gpgme_data_t in, out; |
|
char *encrypted_data = NULL, *edata; |
|
size_t nread; |
|
gpgme_key_t *keys; |
|
gpgme_error_t err; |
|
unsigned i; |
|
|
|
if (!gpg.enabled) |
|
return NULL; |
|
|
|
if (!keyids || !nkeys) { |
|
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); |
|
|
|
keys = g_new0(gpgme_key_t, 1+nkeys); |
|
if (!keys) { |
|
gpgme_release(ctx); |
|
return NULL; |
|
} |
|
|
|
for (i = 0; i < nkeys; i++) { |
|
err = gpgme_get_key(ctx, keyids[i], &keys[i], 0); |
|
if (err || !keys[i]) { |
|
scr_LogPrint(LPRINT_LOGNORM, "GPGME encryption error: cannot use key %s", |
|
keyids[i]); |
|
// We need to have err not null to ensure we won't try to encrypt |
|
// without this key. |
|
if (!err) err = GPG_ERR_UNKNOWN_ERRNO; |
|
break; |
|
} |
|
} |
|
|
|
if (!err) { |
|
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) { |
|
gpgme_data_release(out); |
|
} else { |
|
encrypted_data = gpgme_data_release_and_get_mem(out, &nread); |
|
if (encrypted_data) { |
|
// We need to add a trailing NULL |
|
char *dd = g_strndup(encrypted_data, nread); |
|
free(encrypted_data); |
|
encrypted_data = dd; |
|
} |
|
} |
|
} |
|
gpgme_data_release(in); |
|
} |
|
|
|
if (err && err != GPG_ERR_CANCELED) { |
|
scr_LogPrint(LPRINT_LOGNORM|LPRINT_NOTUTF8, |
|
"GPGME encryption error: %s", gpgme_strerror(err)); |
|
} |
|
} |
|
|
|
for (i = 0; keys[i]; i++) |
|
gpgme_key_release(keys[i]); |
|
g_free(keys); |
|
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; |
|
} |
|
|
|
int gpg_enabled(void) |
|
{ |
|
return gpg.enabled; |
|
} |
|
|
|
#else /* not HAVE_GPGME */ |
|
|
|
int gpg_enabled(void) |
|
{ |
|
return 0; |
|
} |
|
|
|
#endif /* HAVE_GPGME */ |
|
|
|
/* vim: set et cindent cinoptions=>2\:2(0 ts=2 sw=2: For Vim users... */
|
|
|