pidgin/pidgin

facebook: Store sent message id in lastmid, to deduplicate echoed messages

This is crappy and error prone, just like the rest of the duplicate
message handling code. It works, but it's going to get some echoes now
and then, particularly in quick moving groupchats.

I'm pretty sure this is a bug in the server, but the official clients
have much more elaborate deduplication built-in, so they won't notice.
/*
* purple
*
* Purple is the legal property of its developers, whose names are too numerous
* to list here. Please refer to the COPYRIGHT file distributed with this
* source distribution.
*
* 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., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
*
* Written by Tomek Wasilczyk <twasilczyk@pidgin.im>
*/
#include "internal.h"
#include "glibcompat.h"
#include "aescipher.h"
#include "debug.h"
#include "enums.h"
#include <string.h>
#if defined(HAVE_GNUTLS)
# define PURPLE_AES_USE_GNUTLS 1
# include <gnutls/gnutls.h>
# include <gnutls/crypto.h>
#elif defined(HAVE_NSS)
# define PURPLE_AES_USE_NSS 1
# include <nss.h>
# include <pk11pub.h>
# include <prerror.h>
#else
# warning "No GnuTLS or NSS support"
#endif
/* 128bit */
#define PURPLE_AES_BLOCK_SIZE 16
/******************************************************************************
* Structs
*****************************************************************************/
#define PURPLE_AES_CIPHER_GET_PRIVATE(obj) \
(G_TYPE_INSTANCE_GET_PRIVATE((obj), PURPLE_TYPE_AES_CIPHER, PurpleAESCipherPrivate))
typedef struct {
guchar iv[PURPLE_AES_BLOCK_SIZE];
guchar key[32];
guint key_size;
gboolean failure;
PurpleCipherBatchMode batch_mode;
} PurpleAESCipherPrivate;
/******************************************************************************
* Enums
*****************************************************************************/
enum {
PROP_NONE,
PROP_BATCH_MODE,
PROP_IV,
PROP_KEY,
PROP_LAST,
};
/*******************************************************************************
* Globals
******************************************************************************/
static GParamSpec *properties[PROP_LAST];
/******************************************************************************
* Cipher Stuff
*****************************************************************************/
typedef gboolean (*purple_aes_cipher_crypt_func)(
const guchar *input, guchar *output, size_t len,
guchar iv[PURPLE_AES_BLOCK_SIZE], guchar key[32], guint key_size,
PurpleCipherBatchMode batch_mode);
static void
purple_aes_cipher_reset(PurpleCipher *cipher)
{
PurpleAESCipherPrivate *priv = PURPLE_AES_CIPHER_GET_PRIVATE(cipher);
g_return_if_fail(priv != NULL);
memset(priv->iv, 0, sizeof(priv->iv));
memset(priv->key, 0, sizeof(priv->key));
priv->key_size = 32; /* 256bit */
priv->failure = FALSE;
}
static void
purple_aes_cipher_set_iv(PurpleCipher *cipher, guchar *iv, size_t len)
{
PurpleAESCipherPrivate *priv = PURPLE_AES_CIPHER_GET_PRIVATE(cipher);
if ((len > 0 && iv == NULL) ||
(len != 0 && len != sizeof(priv->iv))) {
purple_debug_error("cipher-aes", "invalid IV length\n");
priv->failure = TRUE;
return;
}
if (len == 0)
memset(priv->iv, 0, sizeof(priv->iv));
else
memcpy(priv->iv, iv, len);
g_object_notify_by_pspec(G_OBJECT(cipher), properties[PROP_IV]);
}
static void
purple_aes_cipher_set_key(PurpleCipher *cipher, const guchar *key, size_t len)
{
PurpleAESCipherPrivate *priv = PURPLE_AES_CIPHER_GET_PRIVATE(cipher);
if ((len > 0 && key == NULL) ||
(len != 0 && len != 16 && len != 24 && len != 32)) {
purple_debug_error("cipher-aes", "invalid key length\n");
priv->failure = TRUE;
return;
}
priv->key_size = len;
memset(priv->key, 0, sizeof(priv->key));
if (len > 0)
memcpy(priv->key, key, len);
g_object_notify_by_pspec(G_OBJECT(cipher), properties[PROP_KEY]);
}
static guchar *
purple_aes_cipher_pad_pkcs7(const guchar input[], size_t in_len, size_t *out_len)
{
int padding_len, total_len;
guchar *padded;
g_return_val_if_fail(input != NULL, NULL);
g_return_val_if_fail(out_len != NULL, NULL);
padding_len = PURPLE_AES_BLOCK_SIZE - (in_len % PURPLE_AES_BLOCK_SIZE);
total_len = in_len + padding_len;
g_assert((total_len % PURPLE_AES_BLOCK_SIZE) == 0);
padded = g_new(guchar, total_len);
*out_len = total_len;
memcpy(padded, input, in_len);
memset(padded + in_len, padding_len, padding_len);
return padded;
}
static ssize_t
purple_aes_cipher_unpad_pkcs7(guchar input[], size_t in_len)
{
guchar padding_len, i;
size_t out_len;
g_return_val_if_fail(input != NULL, -1);
g_return_val_if_fail(in_len > 0, -1);
padding_len = input[in_len - 1];
if (padding_len == 0 || padding_len > PURPLE_AES_BLOCK_SIZE ||
padding_len > in_len) {
purple_debug_warning("cipher-aes",
"Invalid padding length: %d (total %" G_GSIZE_FORMAT ") - "
"most probably, the key was invalid\n",
padding_len, in_len);
return -1;
}
out_len = in_len - padding_len;
for (i = 0; i < padding_len; i++) {
if (input[out_len + i] != padding_len) {
purple_debug_warning("cipher-aes",
"Padding doesn't match at pos %d (found %02x, "
"expected %02x) - "
"most probably, the key was invalid\n",
i, input[out_len + i], padding_len);
return -1;
}
}
memset(input + out_len, 0, padding_len);
return out_len;
}
#ifdef PURPLE_AES_USE_GNUTLS
static gnutls_cipher_hd_t
purple_aes_cipher_gnutls_crypt_init(guchar iv[PURPLE_AES_BLOCK_SIZE], guchar key[32],
guint key_size)
{
gnutls_cipher_hd_t handle;
gnutls_cipher_algorithm_t algorithm;
gnutls_datum_t key_info, iv_info;
int ret;
if (key_size == 16)
algorithm = GNUTLS_CIPHER_AES_128_CBC;
else if (key_size == 24)
algorithm = GNUTLS_CIPHER_AES_192_CBC;
else if (key_size == 32)
algorithm = GNUTLS_CIPHER_AES_256_CBC;
else
g_return_val_if_reached(NULL);
key_info.data = key;
key_info.size = key_size;
iv_info.data = iv;
iv_info.size = PURPLE_AES_BLOCK_SIZE;
ret = gnutls_cipher_init(&handle, algorithm, &key_info, &iv_info);
if (ret != 0) {
purple_debug_error("cipher-aes",
"gnutls_cipher_init failed: %d\n", ret);
return NULL;
}
return handle;
}
static gboolean
purple_aes_cipher_gnutls_encrypt(const guchar *input, guchar *output, size_t len,
guchar iv[PURPLE_AES_BLOCK_SIZE], guchar key[32], guint key_size,
PurpleCipherBatchMode batch_mode)
{
gnutls_cipher_hd_t handle;
int ret;
/* We have to simulate ECB mode, which is not supported by GnuTLS. */
if (batch_mode == PURPLE_CIPHER_BATCH_MODE_ECB) {
size_t i;
for (i = 0; i < len / PURPLE_AES_BLOCK_SIZE; i++) {
int offset = i * PURPLE_AES_BLOCK_SIZE;
guchar iv_local[PURPLE_AES_BLOCK_SIZE];
gboolean succ;
memcpy(iv_local, iv, sizeof(iv_local));
succ = purple_aes_cipher_gnutls_encrypt(
input + offset, output + offset,
PURPLE_AES_BLOCK_SIZE,
iv_local, key, key_size,
PURPLE_CIPHER_BATCH_MODE_CBC);
if (!succ)
return FALSE;
}
return TRUE;
}
handle = purple_aes_cipher_gnutls_crypt_init(iv, key, key_size);
if (handle == NULL)
return FALSE;
ret = gnutls_cipher_encrypt2(handle, (guchar *)input, len, output, len);
gnutls_cipher_deinit(handle);
if (ret != 0) {
purple_debug_error("cipher-aes",
"gnutls_cipher_encrypt2 failed: %d\n", ret);
return FALSE;
}
return TRUE;
}
static gboolean
purple_aes_cipher_gnutls_decrypt(const guchar *input, guchar *output, size_t len,
guchar iv[PURPLE_AES_BLOCK_SIZE], guchar key[32], guint key_size,
PurpleCipherBatchMode batch_mode)
{
gnutls_cipher_hd_t handle;
int ret;
/* We have to simulate ECB mode, which is not supported by GnuTLS. */
if (batch_mode == PURPLE_CIPHER_BATCH_MODE_ECB) {
size_t i;
for (i = 0; i < len / PURPLE_AES_BLOCK_SIZE; i++) {
int offset = i * PURPLE_AES_BLOCK_SIZE;
guchar iv_local[PURPLE_AES_BLOCK_SIZE];
gboolean succ;
memcpy(iv_local, iv, sizeof(iv_local));
succ = purple_aes_cipher_gnutls_decrypt(
input + offset, output + offset,
PURPLE_AES_BLOCK_SIZE,
iv_local, key, key_size,
PURPLE_CIPHER_BATCH_MODE_CBC);
if (!succ)
return FALSE;
}
return TRUE;
}
handle = purple_aes_cipher_gnutls_crypt_init(iv, key, key_size);
if (handle == NULL)
return FALSE;
ret = gnutls_cipher_decrypt2(handle, input, len, output, len);
gnutls_cipher_deinit(handle);
if (ret != 0) {
purple_debug_error("cipher-aes",
"gnutls_cipher_decrypt2 failed: %d\n", ret);
return FALSE;
}
return TRUE;
}
#elif defined(PURPLE_AES_USE_NSS)
typedef struct {
PK11SlotInfo *slot;
PK11SymKey *sym_key;
SECItem *sec_param;
PK11Context *enc_context;
} PurpleAESCipherNSSContext;
static void
purple_aes_cipher_nss_cleanup(PurpleAESCipherNSSContext *context)
{
g_return_if_fail(context != NULL);
if (context->enc_context != NULL)
PK11_DestroyContext(context->enc_context, TRUE);
if (context->sec_param != NULL)
SECITEM_FreeItem(context->sec_param, TRUE);
if (context->sym_key != NULL)
PK11_FreeSymKey(context->sym_key);
if (context->slot != NULL)
PK11_FreeSlot(context->slot);
memset(context, 0, sizeof(PurpleAESCipherNSSContext));
}
static gboolean
purple_aes_cipher_nss_crypt(const guchar *input, guchar *output, size_t len,
guchar iv[PURPLE_AES_BLOCK_SIZE], guchar key[32], guint key_size,
CK_ATTRIBUTE_TYPE operation, CK_MECHANISM_TYPE cipher_mech)
{
PurpleAESCipherNSSContext context;
SECItem key_item, iv_item;
SECStatus ret;
int outlen = 0;
unsigned int outlen_tmp = 0;
memset(&context, 0, sizeof(PurpleAESCipherNSSContext));
if (NSS_NoDB_Init(NULL) != SECSuccess) {
purple_debug_error("cipher-aes",
"NSS_NoDB_Init failed: %d\n", PR_GetError());
return FALSE;
}
context.slot = PK11_GetBestSlot(cipher_mech, NULL);
if (context.slot == NULL) {
purple_debug_error("cipher-aes",
"PK11_GetBestSlot failed: %d\n", PR_GetError());
return FALSE;
}
key_item.type = siBuffer;
key_item.data = key;
key_item.len = key_size;
context.sym_key = PK11_ImportSymKey(context.slot, cipher_mech,
PK11_OriginUnwrap, CKA_ENCRYPT, &key_item, NULL);
if (context.sym_key == NULL) {
purple_debug_error("cipher-aes",
"PK11_ImportSymKey failed: %d\n", PR_GetError());
purple_aes_cipher_nss_cleanup(&context);
return FALSE;
}
iv_item.type = siBuffer;
iv_item.data = iv;
iv_item.len = PURPLE_AES_BLOCK_SIZE;
context.sec_param = PK11_ParamFromIV(cipher_mech, &iv_item);
if (context.sec_param == NULL) {
purple_debug_error("cipher-aes",
"PK11_ParamFromIV failed: %d\n", PR_GetError());
purple_aes_cipher_nss_cleanup(&context);
return FALSE;
}
context.enc_context = PK11_CreateContextBySymKey(cipher_mech, operation,
context.sym_key, context.sec_param);
if (context.enc_context == NULL) {
purple_debug_error("cipher-aes",
"PK11_CreateContextBySymKey failed: %d\n",
PR_GetError());
purple_aes_cipher_nss_cleanup(&context);
return FALSE;
}
ret = PK11_CipherOp(context.enc_context, output, &outlen, len,
(guchar *)input, len);
if (ret != SECSuccess) {
purple_debug_error("cipher-aes",
"PK11_CipherOp failed: %d\n", PR_GetError());
purple_aes_cipher_nss_cleanup(&context);
return FALSE;
}
ret = PK11_DigestFinal(context.enc_context, output + outlen, &outlen_tmp,
len - outlen);
if (ret != SECSuccess) {
purple_debug_error("cipher-aes",
"PK11_DigestFinal failed: %d\n", PR_GetError());
purple_aes_cipher_nss_cleanup(&context);
return FALSE;
}
purple_aes_cipher_nss_cleanup(&context);
outlen += outlen_tmp;
if (outlen != (int)len) {
purple_debug_error("cipher-aes",
"resulting length doesn't match: %d (expected: %"
G_GSIZE_FORMAT ")\n", outlen, len);
return FALSE;
}
return TRUE;
}
static CK_MECHANISM_TYPE
purple_aes_cipher_nss_batch_mode(PurpleCipherBatchMode batch_mode)
{
switch (batch_mode) {
case PURPLE_CIPHER_BATCH_MODE_CBC:
return CKM_AES_CBC;
case PURPLE_CIPHER_BATCH_MODE_ECB:
return CKM_AES_ECB;
}
return CKM_AES_CBC;
}
static gboolean
purple_aes_cipher_nss_encrypt(const guchar *input, guchar *output, size_t len,
guchar iv[PURPLE_AES_BLOCK_SIZE], guchar key[32], guint key_size,
PurpleCipherBatchMode batch_mode)
{
return purple_aes_cipher_nss_crypt(input, output, len, iv, key, key_size,
CKA_ENCRYPT, purple_aes_cipher_nss_batch_mode(batch_mode));
}
static gboolean
purple_aes_cipher_nss_decrypt(const guchar *input, guchar *output, size_t len,
guchar iv[PURPLE_AES_BLOCK_SIZE], guchar key[32], guint key_size,
PurpleCipherBatchMode batch_mode)
{
return purple_aes_cipher_nss_crypt(input, output, len, iv, key, key_size,
CKA_DECRYPT, purple_aes_cipher_nss_batch_mode(batch_mode));
}
#endif /* PURPLE_AES_USE_NSS */
static ssize_t
purple_aes_cipher_encrypt(PurpleCipher *cipher, const guchar input[],
size_t in_len, guchar output[], size_t out_size)
{
PurpleAESCipherPrivate *priv = PURPLE_AES_CIPHER_GET_PRIVATE(cipher);
purple_aes_cipher_crypt_func encrypt_func;
guchar *input_padded;
size_t out_len = 0;
gboolean succ;
if (priv->failure)
return -1;
input_padded = purple_aes_cipher_pad_pkcs7(input, in_len, &out_len);
if (out_len > out_size) {
purple_debug_error("cipher-aes", "Output buffer too small (%"
G_GSIZE_FORMAT " > %" G_GSIZE_FORMAT ")",
out_len, out_size);
memset(input_padded, 0, out_len);
g_free(input_padded);
return -1;
}
#if defined(PURPLE_AES_USE_GNUTLS)
encrypt_func = purple_aes_cipher_gnutls_encrypt;
#elif defined(PURPLE_AES_USE_NSS)
encrypt_func = purple_aes_cipher_nss_encrypt;
#else
purple_debug_error("cipher-aes", "No matching encrypt_func\n");
return -1;
#endif
succ = encrypt_func(input_padded, output, out_len, priv->iv,
priv->key, priv->key_size, priv->batch_mode);
memset(input_padded, 0, out_len);
g_free(input_padded);
if (!succ) {
memset(output, 0, out_len);
return -1;
}
return out_len;
}
static ssize_t
purple_aes_cipher_decrypt(PurpleCipher *cipher, const guchar input[],
size_t in_len, guchar output[], size_t out_size)
{
PurpleAESCipherPrivate *priv = PURPLE_AES_CIPHER_GET_PRIVATE(cipher);
purple_aes_cipher_crypt_func decrypt_func;
gboolean succ;
ssize_t out_len;
if (priv->failure)
return -1;
if (in_len > out_size) {
purple_debug_error("cipher-aes", "Output buffer too small\n");
return -1;
}
if ((in_len % PURPLE_AES_BLOCK_SIZE) != 0 || in_len == 0) {
purple_debug_error("cipher-aes", "Malformed data\n");
return -1;
}
#if defined(PURPLE_AES_USE_GNUTLS)
decrypt_func = purple_aes_cipher_gnutls_decrypt;
#elif defined(PURPLE_AES_USE_NSS)
decrypt_func = purple_aes_cipher_nss_decrypt;
#else
purple_debug_error("cipher-aes", "No matching decrypt_func\n");
return -1;
#endif
succ = decrypt_func(input, output, in_len, priv->iv, priv->key,
priv->key_size, priv->batch_mode);
if (!succ) {
memset(output, 0, in_len);
return -1;
}
out_len = purple_aes_cipher_unpad_pkcs7(output, in_len);
if (out_len < 0) {
memset(output, 0, in_len);
return -1;
}
return out_len;
}
static size_t
purple_aes_cipher_get_key_size(PurpleCipher *cipher)
{
PurpleAESCipherPrivate *priv = PURPLE_AES_CIPHER_GET_PRIVATE(cipher);
return priv->key_size;
}
static void
purple_aes_cipher_set_batch_mode(PurpleCipher *cipher,
PurpleCipherBatchMode mode)
{
PurpleAESCipherPrivate *priv = PURPLE_AES_CIPHER_GET_PRIVATE(cipher);
if (mode != PURPLE_CIPHER_BATCH_MODE_CBC &&
mode != PURPLE_CIPHER_BATCH_MODE_ECB)
{
purple_debug_error("cipher-aes", "unsupported batch mode\n");
priv->failure = TRUE;
}
priv->batch_mode = mode;
g_object_notify_by_pspec(G_OBJECT(cipher), properties[PROP_BATCH_MODE]);
}
static PurpleCipherBatchMode
purple_aes_cipher_get_batch_mode(PurpleCipher *cipher)
{
PurpleAESCipherPrivate *priv = PURPLE_AES_CIPHER_GET_PRIVATE(cipher);
return priv->batch_mode;
}
static size_t
purple_aes_cipher_get_block_size(PurpleCipher *cipher)
{
return PURPLE_AES_BLOCK_SIZE;
}
/******************************************************************************
* Object Stuff
*****************************************************************************/
static void
purple_aes_cipher_get_property(GObject *obj, guint param_id, GValue *value,
GParamSpec *pspec)
{
PurpleCipher *cipher = PURPLE_CIPHER(obj);
switch(param_id) {
case PROP_BATCH_MODE:
g_value_set_enum(value,
purple_cipher_get_batch_mode(cipher));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
break;
}
}
static void
purple_aes_cipher_set_property(GObject *obj, guint param_id,
const GValue *value, GParamSpec *pspec)
{
PurpleCipher *cipher = PURPLE_CIPHER(obj);
switch(param_id) {
case PROP_BATCH_MODE:
purple_cipher_set_batch_mode(cipher,
g_value_get_enum(value));
break;
case PROP_IV:
{
guchar *iv = (guchar *)g_value_get_string(value);
purple_cipher_set_iv(cipher, iv, strlen((gchar*)iv));
}
break;
case PROP_KEY:
purple_cipher_set_key(cipher, (guchar *)g_value_get_string(value),
purple_aes_cipher_get_key_size(cipher));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
break;
}
}
static void
purple_aes_cipher_class_init(PurpleAESCipherClass *klass) {
GObjectClass *obj_class = G_OBJECT_CLASS(klass);
PurpleCipherClass *cipher_class = PURPLE_CIPHER_CLASS(klass);
obj_class->get_property = purple_aes_cipher_get_property;
obj_class->set_property = purple_aes_cipher_set_property;
cipher_class->reset = purple_aes_cipher_reset;
cipher_class->set_iv = purple_aes_cipher_set_iv;
cipher_class->encrypt = purple_aes_cipher_encrypt;
cipher_class->decrypt = purple_aes_cipher_decrypt;
cipher_class->set_key = purple_aes_cipher_set_key;
cipher_class->get_key_size = purple_aes_cipher_get_key_size;
cipher_class->set_batch_mode = purple_aes_cipher_set_batch_mode;
cipher_class->get_batch_mode = purple_aes_cipher_get_batch_mode;
cipher_class->get_block_size = purple_aes_cipher_get_block_size;
g_type_class_add_private(klass, sizeof(PurpleAESCipherPrivate));
properties[PROP_BATCH_MODE] = g_param_spec_enum("batch-mode",
"batch-mode", "batch-mode", PURPLE_TYPE_CIPHER_BATCH_MODE,
PURPLE_CIPHER_BATCH_MODE_CBC,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
properties[PROP_IV] = g_param_spec_string("iv", "iv", "iv", NULL,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
properties[PROP_KEY] = g_param_spec_string("key", "key", "key", NULL,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties(obj_class, PROP_LAST, properties);
}
static void
purple_aes_cipher_init(PurpleCipher *cipher) {
purple_cipher_reset(cipher);
}
/******************************************************************************
* API
*****************************************************************************/
GType
purple_aes_cipher_get_type(void) {
static GType type = 0;
if(type == 0) {
static const GTypeInfo info = {
sizeof(PurpleAESCipherClass),
NULL,
NULL,
(GClassInitFunc)purple_aes_cipher_class_init,
NULL,
NULL,
sizeof(PurpleAESCipher),
0,
(GInstanceInitFunc)purple_aes_cipher_init,
NULL
};
type = g_type_register_static(PURPLE_TYPE_CIPHER,
"PurpleAESCipher",
&info, 0);
}
return type;
}
PurpleCipher *
purple_aes_cipher_new(void) {
return g_object_new(PURPLE_TYPE_AES_CIPHER, NULL);
}