summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTomasz Wasilczyk <tomkiewicz@cpw.pidgin.im>2013-05-07 18:11:11 +0200
committerTomasz Wasilczyk <tomkiewicz@cpw.pidgin.im>2013-05-07 18:11:11 +0200
commitc525f9f4c5b8cc004d06d4cfd299ee86fc9239b0 (patch)
tree684af9767ae82a054e35766e4f406e579118aadc
parent60372ec98697473b607859a6d0d1d1bd335d660e (diff)
downloadpidgin-c525f9f4c5b8cc004d06d4cfd299ee86fc9239b0.tar.gz
PBKDF2 support
-rw-r--r--libpurple/cipher.c1
-rw-r--r--libpurple/ciphers/Makefile.am2
-rw-r--r--libpurple/ciphers/ciphers.h3
-rw-r--r--libpurple/ciphers/pbkdf2.c323
-rw-r--r--libpurple/plugins/Makefile.am6
-rw-r--r--libpurple/plugins/ciphertest.c202
6 files changed, 536 insertions, 1 deletions
diff --git a/libpurple/cipher.c b/libpurple/cipher.c
index 256050e8b0..ec1f20f47b 100644
--- a/libpurple/cipher.c
+++ b/libpurple/cipher.c
@@ -255,6 +255,7 @@ purple_ciphers_init() {
purple_ciphers_register_cipher("hmac", purple_hmac_cipher_get_ops());
purple_ciphers_register_cipher("des", purple_des_cipher_get_ops());
purple_ciphers_register_cipher("des3", purple_des3_cipher_get_ops());
+ purple_ciphers_register_cipher("pbkdf2", purple_pbkdf2_cipher_get_ops());
purple_ciphers_register_cipher("rc4", purple_rc4_cipher_get_ops());
}
diff --git a/libpurple/ciphers/Makefile.am b/libpurple/ciphers/Makefile.am
index 67d887694e..e43abdbcd7 100644
--- a/libpurple/ciphers/Makefile.am
+++ b/libpurple/ciphers/Makefile.am
@@ -1,10 +1,12 @@
noinst_LTLIBRARIES=libpurple-ciphers.la
+# XXX: cipher.lo won't be updated after a change in cipher files
libpurple_ciphers_la_SOURCES=\
des.c \
gchecksum.c \
hmac.c \
md4.c \
+ pbkdf2.c \
rc4.c
noinst_HEADERS =\
diff --git a/libpurple/ciphers/ciphers.h b/libpurple/ciphers/ciphers.h
index 333beb2c05..34c6b9aa7b 100644
--- a/libpurple/ciphers/ciphers.h
+++ b/libpurple/ciphers/ciphers.h
@@ -34,5 +34,8 @@ PurpleCipherOps * purple_hmac_cipher_get_ops(void);
/* md4.c */
PurpleCipherOps * purple_md4_cipher_get_ops(void);
+/* pbkdf2.c */
+PurpleCipherOps * purple_pbkdf2_cipher_get_ops(void);
+
/* rc4.c */
PurpleCipherOps * purple_rc4_cipher_get_ops(void);
diff --git a/libpurple/ciphers/pbkdf2.c b/libpurple/ciphers/pbkdf2.c
new file mode 100644
index 0000000000..339ef5168b
--- /dev/null
+++ b/libpurple/ciphers/pbkdf2.c
@@ -0,0 +1,323 @@
+/*
+ * 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 <tomkiewicz@cpw.pidgin.im>
+ */
+
+#include "internal.h"
+#include "cipher.h"
+#include "ciphers.h"
+#include "debug.h"
+
+/* 1024bit */
+#define PBKDF2_HASH_MAX_LEN 128
+
+typedef struct
+{
+ gchar *hash_func;
+ guint iter_count;
+ size_t out_len;
+
+ guchar *salt;
+ size_t salt_len;
+ guchar *passphrase;
+ size_t passphrase_len;
+} Pbkdf2Context;
+
+static void
+purple_pbkdf2_init(PurpleCipherContext *context, void *extra)
+{
+ Pbkdf2Context *ctx_data;
+
+ ctx_data = g_new0(Pbkdf2Context, 1);
+ purple_cipher_context_set_data(context, ctx_data);
+
+ purple_cipher_context_reset(context, extra);
+}
+
+static void
+purple_pbkdf2_uninit(PurpleCipherContext *context)
+{
+ Pbkdf2Context *ctx_data;
+
+ purple_cipher_context_reset(context, NULL);
+
+ ctx_data = purple_cipher_context_get_data(context);
+ g_free(ctx_data);
+ purple_cipher_context_set_data(context, NULL);
+}
+
+static void
+purple_pbkdf2_reset(PurpleCipherContext *context, void *extra)
+{
+ Pbkdf2Context *ctx_data = purple_cipher_context_get_data(context);
+
+ g_return_if_fail(ctx_data != NULL);
+
+ g_free(ctx_data->hash_func);
+ ctx_data->hash_func = NULL;
+ ctx_data->iter_count = 1;
+ ctx_data->out_len = 256;
+
+ purple_cipher_context_reset_state(context, extra);
+}
+
+static void
+purple_pbkdf2_reset_state(PurpleCipherContext *context, void *extra)
+{
+ Pbkdf2Context *ctx_data = purple_cipher_context_get_data(context);
+
+ g_return_if_fail(ctx_data != NULL);
+
+ purple_cipher_context_set_salt(context, NULL, 0);
+ purple_cipher_context_set_key(context, NULL, 0);
+}
+
+static void
+purple_pbkdf2_set_option(PurpleCipherContext *context, const gchar *name,
+ void *value)
+{
+ Pbkdf2Context *ctx_data = purple_cipher_context_get_data(context);
+
+ g_return_if_fail(ctx_data != NULL);
+
+ if (g_strcmp0(name, "hash") == 0) {
+ g_free(ctx_data->hash_func);
+ ctx_data->hash_func = g_strdup(value);
+ return;
+ }
+
+ if (g_strcmp0(name, "iter_count") == 0) {
+ ctx_data->iter_count = GPOINTER_TO_UINT(value);
+ return;
+ }
+
+ if (g_strcmp0(name, "out_len") == 0) {
+ ctx_data->out_len = GPOINTER_TO_UINT(value);
+ return;
+ }
+
+ purple_debug_warning("pbkdf2", "Unknown option: %s\n",
+ name ? name : "(null)");
+}
+
+static void *
+purple_pbkdf2_get_option(PurpleCipherContext *context, const gchar *name)
+{
+ Pbkdf2Context *ctx_data = purple_cipher_context_get_data(context);
+
+ g_return_val_if_fail(ctx_data != NULL, NULL);
+
+ if (g_strcmp0(name, "hash") == 0)
+ return ctx_data->hash_func;
+
+ if (g_strcmp0(name, "iter_count") == 0)
+ return GUINT_TO_POINTER(ctx_data->iter_count);
+
+ if (g_strcmp0(name, "out_len") == 0)
+ return GUINT_TO_POINTER(ctx_data->out_len);
+
+ purple_debug_warning("pbkdf2", "Unknown option: %s\n",
+ name ? name : "(null)");
+ return NULL;
+}
+
+static size_t
+purple_pbkdf2_get_digest_size(PurpleCipherContext *context)
+{
+ Pbkdf2Context *ctx_data = purple_cipher_context_get_data(context);
+
+ g_return_val_if_fail(ctx_data != NULL, 0);
+
+ return ctx_data->out_len;
+}
+
+static void
+purple_pbkdf2_set_salt(PurpleCipherContext *context, const guchar *salt, size_t len)
+{
+ Pbkdf2Context *ctx_data = purple_cipher_context_get_data(context);
+
+ g_return_if_fail(ctx_data != NULL);
+
+ g_free(ctx_data->salt);
+ ctx_data->salt = NULL;
+ ctx_data->salt_len = 0;
+
+ if (len == 0)
+ return;
+ g_return_if_fail(salt != NULL);
+
+ ctx_data->salt = g_memdup(salt, len);
+ ctx_data->salt_len = len;
+}
+
+static void
+purple_pbkdf2_set_key(PurpleCipherContext *context, const guchar *key,
+ size_t len)
+{
+ Pbkdf2Context *ctx_data = purple_cipher_context_get_data(context);
+
+ g_return_if_fail(ctx_data != NULL);
+
+ if (ctx_data->passphrase != NULL) {
+ memset(ctx_data->passphrase, 0, ctx_data->passphrase_len);
+ g_free(ctx_data->passphrase);
+ ctx_data->passphrase = NULL;
+ }
+ ctx_data->passphrase_len = 0;
+
+ if (len == 0)
+ return;
+ g_return_if_fail(key != NULL);
+
+ ctx_data->passphrase = g_memdup(key, len);
+ ctx_data->passphrase_len = len;
+}
+
+/* inspired by gnutls 3.1.10, pbkdf2-sha1.c */
+static gboolean
+purple_pbkdf2_digest(PurpleCipherContext *context, guchar digest[], size_t len)
+{
+ Pbkdf2Context *ctx_data = purple_cipher_context_get_data(context);
+ guchar halfkey[PBKDF2_HASH_MAX_LEN], halfkey_hash[PBKDF2_HASH_MAX_LEN];
+ guint halfkey_len, halfkey_count, halfkey_pad, halfkey_no;
+ guchar *salt_ext;
+ size_t salt_ext_len;
+ guint iter_no;
+ PurpleCipherContext *hash;
+
+ g_return_val_if_fail(ctx_data != NULL, FALSE);
+ g_return_val_if_fail(digest != NULL, FALSE);
+ g_return_val_if_fail(len >= ctx_data->out_len, FALSE);
+
+ g_return_val_if_fail(ctx_data->hash_func != NULL, FALSE);
+ g_return_val_if_fail(ctx_data->iter_count > 0, FALSE);
+ g_return_val_if_fail(ctx_data->passphrase != NULL ||
+ ctx_data->passphrase_len == 0, FALSE);
+ g_return_val_if_fail(ctx_data->salt != NULL || ctx_data->salt_len == 0,
+ FALSE);
+ g_return_val_if_fail(ctx_data->out_len > 0, FALSE);
+ g_return_val_if_fail(ctx_data->out_len < 0xFFFFFFFFU, FALSE);
+
+ salt_ext_len = ctx_data->salt_len + 4;
+
+ hash = purple_cipher_context_new_by_name("hmac", NULL);
+ if (hash == NULL) {
+ purple_debug_error("pbkdf2", "Couldn't create new hmac "
+ "context\n");
+ return FALSE;
+ }
+ purple_cipher_context_set_option(hash, "hash",
+ (void*)ctx_data->hash_func);
+ purple_cipher_context_set_key(hash, (const guchar*)ctx_data->passphrase,
+ ctx_data->passphrase_len);
+
+ halfkey_len = purple_cipher_context_get_digest_size(hash);
+ if (halfkey_len <= 0 || halfkey_len > PBKDF2_HASH_MAX_LEN) {
+ purple_debug_error("pbkdf2", "Unsupported hash function: %s "
+ "(digest size: %d)\n",
+ ctx_data->hash_func ? ctx_data->hash_func : "(null)",
+ halfkey_len);
+ return FALSE;
+ }
+
+ halfkey_count = ((ctx_data->out_len - 1) / halfkey_len) + 1;
+ halfkey_pad = ctx_data->out_len - (halfkey_count - 1) * halfkey_len;
+
+ salt_ext = g_new(guchar, salt_ext_len);
+ memcpy(salt_ext, ctx_data->salt, ctx_data->salt_len);
+
+ for (halfkey_no = 1; halfkey_no <= halfkey_count; halfkey_no++) {
+ memset(halfkey, 0, halfkey_len);
+
+ for (iter_no = 1; iter_no <= ctx_data->iter_count; iter_no++) {
+ int i;
+
+ purple_cipher_context_reset_state(hash, NULL);
+
+ if (iter_no == 1) {
+ salt_ext[salt_ext_len - 4] =
+ (halfkey_no & 0xff000000) >> 24;
+ salt_ext[salt_ext_len - 3] =
+ (halfkey_no & 0x00ff0000) >> 16;
+ salt_ext[salt_ext_len - 2] =
+ (halfkey_no & 0x0000ff00) >> 8;
+ salt_ext[salt_ext_len - 1] =
+ (halfkey_no & 0x000000ff) >> 0;
+
+ purple_cipher_context_append(hash, salt_ext,
+ salt_ext_len);
+ }
+ else
+ purple_cipher_context_append(hash, halfkey_hash,
+ halfkey_len);
+
+ if (!purple_cipher_context_digest(hash, halfkey_hash,
+ halfkey_len)) {
+ purple_debug_error("pbkdf2",
+ "Couldn't retrieve a digest\n");
+ g_free(salt_ext);
+ purple_cipher_context_destroy(hash);
+ return FALSE;
+ }
+
+ for (i = 0; i < halfkey_len; i++)
+ halfkey[i] ^= halfkey_hash[i];
+ }
+
+ memcpy(digest + (halfkey_no - 1) * halfkey_len, halfkey,
+ (halfkey_no == halfkey_count) ? halfkey_pad :
+ halfkey_len);
+ }
+
+ g_free(salt_ext);
+ purple_cipher_context_destroy(hash);
+
+ return TRUE;
+}
+
+static PurpleCipherOps PBKDF2Ops = {
+ purple_pbkdf2_set_option, /* set_option */
+ purple_pbkdf2_get_option, /* get_option */
+ purple_pbkdf2_init, /* init */
+ purple_pbkdf2_reset, /* reset */
+ purple_pbkdf2_reset_state, /* reset_state */
+ purple_pbkdf2_uninit, /* uninit */
+ NULL, /* set_iv */
+ NULL, /* append */
+ purple_pbkdf2_digest, /* digest */
+ purple_pbkdf2_get_digest_size, /* get_digest_size */
+ NULL, /* encrypt */
+ NULL, /* decrypt */
+ purple_pbkdf2_set_salt, /* set_salt */
+ NULL, /* get_salt_size */
+ purple_pbkdf2_set_key, /* set_key */
+ NULL, /* get_key_size */
+ NULL, /* set_batch_mode */
+ NULL, /* get_batch_mode */
+ NULL, /* get_block_size */
+ NULL, NULL, NULL, NULL /* reserved */
+};
+
+PurpleCipherOps *
+purple_pbkdf2_cipher_get_ops(void) {
+ return &PBKDF2Ops;
+}
diff --git a/libpurple/plugins/Makefile.am b/libpurple/plugins/Makefile.am
index 08b146424a..cc3eae6a31 100644
--- a/libpurple/plugins/Makefile.am
+++ b/libpurple/plugins/Makefile.am
@@ -147,7 +147,11 @@ AM_CPPFLAGS = \
$(DEBUG_CFLAGS) \
$(GLIB_CFLAGS) \
$(PLUGIN_CFLAGS) \
- $(DBUS_CFLAGS)
+ $(DBUS_CFLAGS) \
+ $(NSS_CFLAGS)
+
+PLUGIN_LIBS = \
+ $(NSS_LIBS)
#
# This part allows people to build their own plugins in here.
diff --git a/libpurple/plugins/ciphertest.c b/libpurple/plugins/ciphertest.c
index dd16e023b7..4b1e278f5a 100644
--- a/libpurple/plugins/ciphertest.c
+++ b/libpurple/plugins/ciphertest.c
@@ -231,6 +231,207 @@ cipher_test_digest(void)
}
/**************************************************************************
+ * PBKDF2 stuff
+ **************************************************************************/
+
+#include <nss.h>
+#include <secmod.h>
+#include <pk11func.h>
+#include <prerror.h>
+#include <secerr.h>
+
+typedef struct {
+ const gchar *hash;
+ const guint iter_count;
+ const gchar *passphrase;
+ const gchar *salt;
+ const guint out_len;
+ const gchar *answer;
+} pbkdf2_test;
+
+pbkdf2_test pbkdf2_tests[] = {
+ { "sha256", 1, "password", "salt", 32, "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b"},
+ { "sha1", 1, "password", "salt", 32, "0c60c80f961f0e71f3a9b524af6012062fe037a6e0f0eb94fe8fc46bdc637164"},
+ { "sha1", 1000, "ala ma kota", "", 16, "924dba137b5bcf6d0de84998f3d8e1f9"},
+ { "sha1", 1, "", "", 32, "1e437a1c79d75be61e91141dae20affc4892cc99abcc3fe753887bccc8920176"},
+ { "sha256", 100, "some password", "and salt", 1, "c7"},
+ { "sha1", 10000, "pretty long password W Szczebrzeszynie chrzaszcz brzmi w trzcinie i Szczebrzeszyn z tego slynie", "Grzegorz Brzeczyszczykiewicz", 32, "8cb0cb164f2554733ae02f5751b0e84a88fb385446e85a3991bdcdf1ea11795c"},
+ { NULL, 0, NULL, NULL, 0, NULL}
+};
+
+static gchar*
+cipher_pbkdf2_nss_sha1(const gchar *passphrase, const gchar *salt,
+ guint iter_count, guint out_len)
+{
+ PK11SlotInfo *slot;
+ SECAlgorithmID *algorithm = NULL;
+ PK11SymKey *symkey = NULL;
+ const SECItem *symkey_data = NULL;
+ SECItem salt_item, passphrase_item;
+ guchar *passphrase_buff, *salt_buff;
+ gchar *ret;
+
+ g_return_val_if_fail(passphrase != NULL, NULL);
+ g_return_val_if_fail(iter_count > 0, NULL);
+ g_return_val_if_fail(out_len > 0, NULL);
+
+ NSS_NoDB_Init(NULL);
+
+ slot = PK11_GetBestSlot(PK11_AlgtagToMechanism(SEC_OID_PKCS5_PBKDF2),
+ NULL);
+ if (slot == NULL) {
+ purple_debug_error("cipher-test", "NSS: couldn't get slot: "
+ "%d\n", PR_GetError());
+ return NULL;
+ }
+
+ salt_buff = (guchar*)g_strdup(salt ? salt : "");
+ salt_item.type = siBuffer;
+ salt_item.data = salt_buff;
+ salt_item.len = salt ? strlen(salt) : 0;
+
+ algorithm = PK11_CreatePBEV2AlgorithmID(SEC_OID_PKCS5_PBKDF2,
+ SEC_OID_AES_256_CBC, SEC_OID_HMAC_SHA1, out_len, iter_count,
+ &salt_item);
+ if (algorithm == NULL) {
+ purple_debug_error("cipher-test", "NSS: couldn't create "
+ "algorithm ID: %d\n", PR_GetError());
+ PK11_FreeSlot(slot);
+ g_free(salt_buff);
+ return NULL;
+ }
+
+ passphrase_buff = (guchar*)g_strdup(passphrase);
+ passphrase_item.type = siBuffer;
+ passphrase_item.data = passphrase_buff;
+ passphrase_item.len = strlen(passphrase);
+
+ symkey = PK11_PBEKeyGen(slot, algorithm, &passphrase_item, PR_FALSE,
+ NULL);
+ if (symkey == NULL) {
+ purple_debug_error("cipher-test", "NSS: Couldn't generate key: "
+ "%d\n", PR_GetError());
+ SECOID_DestroyAlgorithmID(algorithm, PR_TRUE);
+ PK11_FreeSlot(slot);
+ g_free(passphrase_buff);
+ g_free(salt_buff);
+ return NULL;
+ }
+
+ if (PK11_ExtractKeyValue(symkey) == SECSuccess)
+ symkey_data = PK11_GetKeyData(symkey);
+
+ if (symkey_data == NULL || symkey_data->data == NULL) {
+ purple_debug_error("cipher-test", "NSS: Couldn't extract key "
+ "value: %d\n", PR_GetError());
+ PK11_FreeSymKey(symkey);
+ SECOID_DestroyAlgorithmID(algorithm, PR_TRUE);
+ PK11_FreeSlot(slot);
+ g_free(passphrase_buff);
+ g_free(salt_buff);
+ return NULL;
+ }
+
+ if (symkey_data->len != out_len) {
+ purple_debug_error("cipher-test", "NSS: Invalid key length: %d "
+ "(should be %d)\n", symkey_data->len, out_len);
+ PK11_FreeSymKey(symkey);
+ SECOID_DestroyAlgorithmID(algorithm, PR_TRUE);
+ PK11_FreeSlot(slot);
+ g_free(passphrase_buff);
+ g_free(salt_buff);
+ return NULL;
+ }
+
+ ret = purple_base16_encode(symkey_data->data, symkey_data->len);
+
+ PK11_FreeSymKey(symkey);
+ SECOID_DestroyAlgorithmID(algorithm, PR_TRUE);
+ PK11_FreeSlot(slot);
+ g_free(passphrase_buff);
+ g_free(salt_buff);
+ return ret;
+}
+
+static void
+cipher_test_pbkdf2(void)
+{
+ PurpleCipherContext *context;
+ int i = 0;
+ gboolean fail = FALSE;
+
+ purple_debug_info("cipher-test", "Running PBKDF2 tests\n");
+
+ context = purple_cipher_context_new_by_name("pbkdf2", NULL);
+
+ while (pbkdf2_tests[i].answer) {
+ pbkdf2_test *test = &pbkdf2_tests[i];
+ gchar digest[2 * 32 + 1 + 10];
+ gchar *digest_nss = NULL;
+ gboolean ret, skip_nss = FALSE;
+
+ i++;
+
+ purple_debug_info("cipher-test", "Test %02d:\n", i);
+ purple_debug_info("cipher-test",
+ "\tTesting '%s' with salt:'%s' hash:%s iter_count:%d \n",
+ test->passphrase, test->salt, test->hash,
+ test->iter_count);
+
+ purple_cipher_context_set_option(context, "hash", (gpointer)test->hash);
+ purple_cipher_context_set_option(context, "iter_count", GUINT_TO_POINTER(test->iter_count));
+ purple_cipher_context_set_option(context, "out_len", GUINT_TO_POINTER(test->out_len));
+ purple_cipher_context_set_salt(context, (const guchar*)test->salt, test->salt ? strlen(test->salt): 0);
+ purple_cipher_context_set_key(context, (const guchar*)test->passphrase, strlen(test->passphrase));
+
+ ret = purple_cipher_context_digest_to_str(context, digest, sizeof(digest));
+ purple_cipher_context_reset(context, NULL);
+
+ if (!ret) {
+ purple_debug_info("cipher-test", "\tfailed\n");
+ fail = TRUE;
+ continue;
+ }
+
+ if (g_strcmp0(test->hash, "sha1") != 0)
+ skip_nss = TRUE;
+ if (test->out_len != 16 && test->out_len != 32)
+ skip_nss = TRUE;
+
+ if (!skip_nss) {
+ digest_nss = cipher_pbkdf2_nss_sha1(test->passphrase,
+ test->salt, test->iter_count, test->out_len);
+ }
+
+ if (!ret) {
+ purple_debug_info("cipher-test", "\tnss test failed\n");
+ fail = TRUE;
+ }
+
+ if (g_strcmp0(digest, test->answer) == 0 &&
+ (skip_nss || g_strcmp0(digest, digest_nss) == 0)) {
+ purple_debug_info("cipher-test", "\tTest OK\n");
+ }
+ else {
+ purple_debug_info("cipher-test", "\twrong answer\n");
+ fail = TRUE;
+ }
+
+ purple_debug_info("cipher-test", "\tGot: %s\n", digest);
+ if (digest_nss)
+ purple_debug_info("cipher-test", "\tGot from NSS: %s\n", digest_nss);
+ purple_debug_info("cipher-test", "\tWanted: %s\n", test->answer);
+ }
+
+ purple_cipher_context_destroy(context);
+
+ if (fail)
+ purple_debug_info("cipher-test", "PBKDF2 tests FAILED\n\n");
+ else
+ purple_debug_info("cipher-test", "PBKDF2 tests completed successfully\n\n");
+}
+
+/**************************************************************************
* Plugin stuff
**************************************************************************/
static gboolean
@@ -238,6 +439,7 @@ plugin_load(PurplePlugin *plugin) {
cipher_test_md5();
cipher_test_sha1();
cipher_test_digest();
+ cipher_test_pbkdf2();
return TRUE;
}