From c5a798beec0f24f6828cd3c43bbefd1e18a9d33a Mon Sep 17 00:00:00 2001 From: Bakasura Date: Mon, 13 Jul 2020 18:27:27 -0500 Subject: [PATCH] FiSHLiM: Support for CBC mode + more commands (#2347) --- plugins/fishlim/fish.c | 457 ++++++++++++++---- plugins/fishlim/fish.h | 17 +- plugins/fishlim/fishlim.vcxproj.filters | 6 + plugins/fishlim/keystore.c | 61 ++- plugins/fishlim/keystore.h | 5 +- plugins/fishlim/meson.build | 3 + plugins/fishlim/plugin_hexchat.c | 342 ++++++++----- plugins/fishlim/tests/fake/keystore.c | 47 ++ plugins/fishlim/tests/meson.build | 17 + plugins/fishlim/tests/old_version/fish.c | 194 ++++++++ plugins/fishlim/tests/old_version/fish.h | 39 ++ plugins/fishlim/tests/old_version/meson.build | 4 + plugins/fishlim/tests/tests.c | 248 ++++++++++ plugins/fishlim/utils.c | 70 +++ plugins/fishlim/utils.h | 35 ++ 15 files changed, 1307 insertions(+), 238 deletions(-) create mode 100644 plugins/fishlim/tests/fake/keystore.c create mode 100644 plugins/fishlim/tests/meson.build create mode 100644 plugins/fishlim/tests/old_version/fish.c create mode 100644 plugins/fishlim/tests/old_version/fish.h create mode 100644 plugins/fishlim/tests/old_version/meson.build create mode 100644 plugins/fishlim/tests/tests.c create mode 100644 plugins/fishlim/utils.c create mode 100644 plugins/fishlim/utils.h diff --git a/plugins/fishlim/fish.c b/plugins/fishlim/fish.c index 00ecfbfa..9301d5d5 100644 --- a/plugins/fishlim/fish.c +++ b/plugins/fishlim/fish.c @@ -1,6 +1,7 @@ /* Copyright (c) 2010 Samuel Lidén Borell + Copyright (c) 2019 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -27,15 +28,19 @@ #define DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER #endif +#include #include #include +#include #include +#include #include "keystore.h" #include "fish.h" #define IB 64 -static const char fish_base64[64] = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +static const char fish_base64[] = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; static const signed char fish_unbase64[256] = { IB,IB,IB,IB,IB,IB,IB,IB, IB,IB,IB,IB,IB,IB,IB,IB, IB,IB,IB,IB,IB,IB,IB,IB, IB,IB,IB,IB,IB,IB,IB,IB, @@ -53,6 +58,12 @@ static const signed char fish_unbase64[256] = { 27,28,29,30,31,32,33,34, 35,36,37,IB,IB,IB,IB,IB, }; +/** + * Convert Int to 4 Bytes (Big-endian) + * + * @param int source + * @param char* dest + */ #define GET_BYTES(dest, source) do { \ *((dest)++) = ((source) >> 24) & 0xFF; \ *((dest)++) = ((source) >> 16) & 0xFF; \ @@ -60,135 +71,387 @@ static const signed char fish_unbase64[256] = { *((dest)++) = (source) & 0xFF; \ } while (0); +/** + * Convert 4 Bytes to Int (Big-endian) + * + * @param char* source + * @param int dest + */ +#define GET_LONG(dest, source) do { \ + dest = ((uint8_t)*((source)++) << 24); \ + dest |= ((uint8_t)*((source)++) << 16); \ + dest |= ((uint8_t)*((source)++) << 8); \ + dest |= (uint8_t)*((source)++); \ +} while (0); -char *fish_encrypt(const char *key, size_t keylen, const char *message) { - BF_KEY bfkey; - size_t messagelen; - size_t i; - int j; - char *encrypted; - char *end; - unsigned char bit; - unsigned char word; - unsigned char d; - BF_set_key(&bfkey, keylen, (const unsigned char*)key); - - messagelen = strlen(message); - if (messagelen == 0) return NULL; - encrypted = g_malloc(((messagelen - 1) / 8) * 12 + 12 + 1); /* each 8-byte block becomes 12 bytes */ - end = encrypted; - - while (*message) { - /* Read 8 bytes (a Blowfish block) */ - BF_LONG binary[2] = { 0, 0 }; - unsigned char c; - for (i = 0; i < 8; i++) { - c = message[i]; - binary[i >> 2] |= c << 8*(3 - (i&3)); - if (c == '\0') break; + +/** + * Encode ECB FiSH Base64 + * + * @param [in] message Bytes to encode + * @param [in] message_len Size of bytes to encode + * @return Array of char with encoded string + */ +char *fish_base64_encode(const char *message, size_t message_len) { + BF_LONG left = 0, right = 0; + int i, j; + char *encoded = NULL; + char *end = NULL; + char *msg = NULL; + + if (message_len == 0) + return NULL; + + /* Each 8-byte block becomes 12 bytes (fish base64 format) and add 1 byte for \0 */ + encoded = g_malloc(((message_len - 1) / 8) * 12 + 12 + 1); + end = encoded; + + /* Iterate over each 8-byte block (Blowfish block size) */ + for (j = 0; j < message_len; j += 8) { + msg = (char *) message; + + /* Set left and right longs */ + GET_LONG(left, msg); + GET_LONG(right, msg); + + for (i = 0; i < 6; ++i) { + *end++ = fish_base64[right & 0x3fu]; + right = (right >> 6u); } + + for (i = 0; i < 6; ++i) { + *end++ = fish_base64[left & 0x3fu]; + left = (left >> 6u); + } + + /* The previous for'd ensure fill all bytes of encoded, we don't need will with zeros */ message += 8; - - /* Encrypt block */ - BF_encrypt(binary, &bfkey); - - /* Emit FiSH-BASE64 */ - bit = 0; - word = 1; - for (j = 0; j < 12; j++) { - d = fish_base64[(binary[word] >> bit) & 63]; - *(end++) = d; - bit += 6; - if (j == 5) { - bit = 0; - word = 0; - } - } - - /* Stop if a null terminator was found */ - if (c == '\0') break; } + *end = '\0'; - return encrypted; + return encoded; } +/** + * Decode ECB FiSH Base64 + * + * @param [in] message Base64 encoded string + * @param [out] final_len Real length of message + * @return Array of char with decoded message + */ +char *fish_base64_decode(const char *message, size_t *final_len) { + BF_LONG left, right; + int i; + char *bytes = NULL; + char *msg = NULL; + char *byt = NULL; + size_t message_len; -char *fish_decrypt(const char *key, size_t keylen, const char *data) { - BF_KEY bfkey; - size_t i; - char *decrypted; - char *end; - unsigned char bit; - unsigned char word; - unsigned char d; - BF_set_key(&bfkey, keylen, (const unsigned char*)key); - - decrypted = g_malloc(strlen(data) + 1); - end = decrypted; - - while (*data) { - /* Convert from FiSH-BASE64 */ - BF_LONG binary[2] = { 0, 0 }; - bit = 0; - word = 1; - for (i = 0; i < 12; i++) { - d = fish_unbase64[(const unsigned char)*(data++)]; - if (d == IB) goto decrypt_end; - binary[word] |= (unsigned long)d << bit; - bit += 6; - if (i == 5) { - bit = 0; - word = 0; - } - } - - /* Decrypt block */ - BF_decrypt(binary, &bfkey); - - /* Copy to buffer */ - GET_BYTES(end, binary[0]); - GET_BYTES(end, binary[1]); + message_len = strlen(message); + + /* Ensure blocks of 12 bytes each one and valid characters */ + if (message_len == 0 || message_len % 12 != 0 || strspn(message, fish_base64) != message_len) + return NULL; + + /* Each 12 bytes becomes 8-byte block and add 1 byte for \0 */ + *final_len = ((message_len - 1) / 12) * 8 + 8 + 1; + (*final_len)--; /* We support binary data */ + bytes = (char *) g_malloc0(*final_len); + byt = bytes; + + msg = (char *) message; + + while (*msg) { + right = 0; + left = 0; + for (i = 0; i < 6; i++) right |= (uint8_t) fish_unbase64[*msg++] << (i * 6u); + for (i = 0; i < 6; i++) left |= (uint8_t) fish_unbase64[*msg++] << (i * 6u); + GET_BYTES(byt, left); + GET_BYTES(byt, right); } - - decrypt_end: - *end = '\0'; - return decrypted; + + return bytes; +} + +/** + * Encrypt or decrypt data with Blowfish cipher, support binary data. + * + * Good documentation for EVP: + * + * - https://wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption + * + * - https://stackoverflow.com/questions/5727646/what-is-the-length-parameter-of-aes-evp-decrypt + * + * - https://stackoverflow.com/questions/26345175/correct-way-to-free-allocate-the-context-in-the-openssl + * + * - https://stackoverflow.com/questions/29874150/working-with-evp-and-openssl-coding-in-c + * + * @param [in] plaintext Bytes to encrypt or decrypt + * @param [in] plaintext_len Size of plaintext + * @param [in] key Bytes of key + * @param [in] keylen Size of key + * @param [in] encode 1 or encrypt 0 for decrypt + * @param [in] mode EVP_CIPH_ECB_MODE or EVP_CIPH_CBC_MODE + * @param [out] ciphertext_len The bytes written + * @return Array of char with data encrypted or decrypted + */ +char *fish_cipher(const char *plaintext, size_t plaintext_len, const char *key, size_t keylen, int encode, int mode, size_t *ciphertext_len) { + EVP_CIPHER_CTX *ctx; + EVP_CIPHER *cipher = NULL; + int bytes_written = 0; + unsigned char *ciphertext = NULL; + unsigned char *iv_ciphertext = NULL; + unsigned char *iv = NULL; + size_t block_size = 0; + + *ciphertext_len = 0; + + if (plaintext_len == 0 || keylen == 0 || encode < 0 || encode > 1) + return NULL; + + block_size = plaintext_len; + + if (mode == EVP_CIPH_CBC_MODE) { + if (encode == 1) { + iv = (unsigned char *) g_malloc0(8); + RAND_bytes(iv, 8); + } else { + if (plaintext_len <= 8) /* IV + DATA */ + return NULL; + + iv = (unsigned char *) plaintext; + block_size -= 8; + plaintext += 8; + plaintext_len -= 8; + } + + cipher = (EVP_CIPHER *) EVP_bf_cbc(); + } else if (mode == EVP_CIPH_ECB_MODE) { + cipher = (EVP_CIPHER *) EVP_bf_ecb(); + } + + /* Zero Padding */ + if (block_size % 8 != 0) { + block_size = block_size + 8 - (block_size % 8); + } + + ciphertext = (unsigned char *) g_malloc0(block_size); + memcpy(ciphertext, plaintext, plaintext_len); + + /* Create and initialise the context */ + if (!(ctx = EVP_CIPHER_CTX_new())) + return NULL; + + /* Initialise the cipher operation only with mode */ + if (!EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, encode)) + return NULL; + + /* Set custom key length */ + if (!EVP_CIPHER_CTX_set_key_length(ctx, keylen)) + return NULL; + + /* Finish the initiation the cipher operation */ + if (1 != EVP_CipherInit_ex(ctx, NULL, NULL, (const unsigned char *) key, iv, encode)) + return NULL; + + /* We will manage this */ + EVP_CIPHER_CTX_set_padding(ctx, 0); + + /* Do cipher operation */ + if (1 != EVP_CipherUpdate(ctx, ciphertext, &bytes_written, ciphertext, block_size)) + return NULL; + + *ciphertext_len = bytes_written; + + /* Finalise the cipher. Further ciphertext bytes may be written at this stage */ + if (1 != EVP_CipherFinal_ex(ctx, ciphertext + bytes_written, &bytes_written)) + return NULL; + + *ciphertext_len += bytes_written; + + /* Clean up */ + EVP_CIPHER_CTX_free(ctx); + + + if (mode == EVP_CIPH_CBC_MODE && encode == 1) { + /* Join IV + DATA */ + iv_ciphertext = g_malloc0(8 + *ciphertext_len); + + memcpy(iv_ciphertext, iv, 8); + memcpy(&iv_ciphertext[8], ciphertext, *ciphertext_len); + *ciphertext_len += 8; + + g_free(ciphertext); + g_free(iv); + + return (char *) iv_ciphertext; + } else { + return (char *) ciphertext; + } +} + +/** + * Return a fish or standard Base64 encoded string with data encrypted + * is binary safe + * + * @param [in] key Bytes of key + * @param [in] keylen Size of key + * @param [in] message Bytes to encrypt + * @param [in] message_len Size of message + * @param [in] mode Chiper mode + * @return Array of char with data encrypted + */ +char *fish_encrypt(const char *key, size_t keylen, const char *message, size_t message_len, enum fish_mode mode) { + size_t ciphertext_len = 0; + char *ciphertext = NULL; + char *b64 = NULL; + + if (keylen == 0 || message_len == 0) + return NULL; + + ciphertext = fish_cipher(message, message_len, key, keylen, 1, mode, &ciphertext_len); + + if (ciphertext == NULL || ciphertext_len == 0) + return NULL; + + switch (mode) { + case FISH_CBC_MODE: + b64 = g_base64_encode((const unsigned char *) ciphertext, ciphertext_len); + break; + + case FISH_ECB_MODE: + b64 = fish_base64_encode((const char *) ciphertext, ciphertext_len); + } + + g_free(ciphertext); + + if (b64 == NULL) + return NULL; + + return b64; +} + +/** + * Return an array of bytes with data decrypted + * is binary safe + * + * @param [in] key Bytes of key + * @param [in] keylen Size of key + * @param [in] data Fish or standard Base64 encoded string + * @param [in] mode Chiper mode + * @param [out] final_len Length of returned array + * @return Array of char with data decrypted + */ +char *fish_decrypt(const char *key, size_t keylen, const char *data, enum fish_mode mode, size_t *final_len) { + size_t ciphertext_len = 0; + char *ciphertext = NULL; + char *plaintext = NULL; + + *final_len = 0; + + if (keylen == 0 || strlen(data) == 0) + return NULL; + + switch (mode) { + case FISH_CBC_MODE: + if (strspn(data, base64_chars) != strlen(data)) + return NULL; + ciphertext = (char *) g_base64_decode(data, &ciphertext_len); + break; + + case FISH_ECB_MODE: + ciphertext = fish_base64_decode(data, &ciphertext_len); + } + + if (ciphertext == NULL || ciphertext_len == 0) + return NULL; + + plaintext = fish_cipher(ciphertext, ciphertext_len, key, keylen, 0, mode, final_len); + g_free(ciphertext); + + if (*final_len == 0) + return NULL; + + return plaintext; +} + +/** + * Similar to fish_decrypt, but pad with zeros any after the first zero in the decrypted data + * + * @param [in] key Bytes of key + * @param [in] keylen Size of key + * @param [in] data Fish or standard Base64 encoded string + * @param [in] mode Chiper mode + * @return Array of char with data decrypted + */ +char *fish_decrypt_str(const char *key, size_t keylen, const char *data, enum fish_mode mode) { + char *decrypted = NULL; + char *plaintext_str = NULL; + size_t decrypted_len = 0; + + decrypted = fish_decrypt(key, strlen(key), data, mode, &decrypted_len); + + if (decrypted == NULL || decrypted_len == 0) + return NULL; + + plaintext_str = g_strndup(decrypted, decrypted_len); + g_free(decrypted); + + return plaintext_str; } /** * Encrypts a message (see fish_decrypt). The key is searched for in the * key store. */ -char *fish_encrypt_for_nick(const char *nick, const char *data) { +char *fish_encrypt_for_nick(const char *nick, const char *data, enum fish_mode *omode) { char *key; - char *encrypted; + char *encrypted, *encrypted_cbc = NULL; + enum fish_mode mode; /* Look for key */ - key = keystore_get_key(nick); + key = keystore_get_key(nick, &mode); if (!key) return NULL; - + + *omode = mode; + /* Encrypt */ - encrypted = fish_encrypt(key, strlen(key), data); - + encrypted = fish_encrypt(key, strlen(key), data, strlen(data), mode); + g_free(key); - return encrypted; + + if (encrypted == NULL || mode == FISH_ECB_MODE) + return encrypted; + + /* Add '*' for CBC */ + encrypted_cbc = g_strdup_printf("*%s",encrypted); + g_free(encrypted); + + return encrypted_cbc; } /** * Decrypts a message (see fish_decrypt). The key is searched for in the * key store. */ -char *fish_decrypt_from_nick(const char *nick, const char *data) { +char *fish_decrypt_from_nick(const char *nick, const char *data, enum fish_mode *omode) { char *key; char *decrypted; + enum fish_mode mode; + /* Look for key */ - key = keystore_get_key(nick); + key = keystore_get_key(nick, &mode); if (!key) return NULL; - + + *omode = mode; + + if (mode == FISH_CBC_MODE) + ++data; + /* Decrypt */ - decrypted = fish_decrypt(key, strlen(key), data); - + decrypted = fish_decrypt_str(key, strlen(key), data, mode); g_free(key); + return decrypted; } diff --git a/plugins/fishlim/fish.h b/plugins/fishlim/fish.h index 238f52e7..daf17acf 100644 --- a/plugins/fishlim/fish.h +++ b/plugins/fishlim/fish.h @@ -1,6 +1,7 @@ /* Copyright (c) 2010 Samuel Lidén Borell + Copyright (c) 2019 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -29,10 +30,18 @@ #include -char *fish_encrypt(const char *key, size_t keylen, const char *message); -char *fish_decrypt(const char *key, size_t keylen, const char *data); -char *fish_encrypt_for_nick(const char *nick, const char *data); -char *fish_decrypt_from_nick(const char *nick, const char *data); +enum fish_mode { + FISH_ECB_MODE = 0x1, + FISH_CBC_MODE = 0x2 +}; + +char *fish_base64_encode(const char *message, size_t message_len); +char *fish_base64_decode(const char *message, size_t *final_len); +char *fish_encrypt(const char *key, size_t keylen, const char *message, size_t message_len, enum fish_mode mode); +char *fish_decrypt(const char *key, size_t keylen, const char *data, enum fish_mode mode, size_t *final_len); +char *fish_decrypt_str(const char *key, size_t keylen, const char *data, enum fish_mode mode); +char *fish_encrypt_for_nick(const char *nick, const char *data, enum fish_mode *omode); +char *fish_decrypt_from_nick(const char *nick, const char *data, enum fish_mode *omode); #endif diff --git a/plugins/fishlim/fishlim.vcxproj.filters b/plugins/fishlim/fishlim.vcxproj.filters index d8fbf454..09649ec9 100644 --- a/plugins/fishlim/fishlim.vcxproj.filters +++ b/plugins/fishlim/fishlim.vcxproj.filters @@ -23,6 +23,9 @@ Header Files + + Header Files + Header Files @@ -37,6 +40,9 @@ + + Source Files + Source Files diff --git a/plugins/fishlim/keystore.c b/plugins/fishlim/keystore.c index 061faa03..adefc700 100644 --- a/plugins/fishlim/keystore.c +++ b/plugins/fishlim/keystore.c @@ -103,27 +103,55 @@ static gchar *get_nick_value(GKeyFile *keyfile, const char *nick, const char *it /** * Extracts a key from the key store file. */ -char *keystore_get_key(const char *nick) { +char *keystore_get_key(const char *nick, enum fish_mode *mode) { + GKeyFile *keyfile; + char *escaped_nick; + gchar *value, *key_mode; + int encrypted_mode; + char *password; + char *encrypted; + char *decrypted; + /* Get the key */ - GKeyFile *keyfile = getConfigFile(); - char *escaped_nick = escape_nickname(nick); - gchar *value = get_nick_value(keyfile, escaped_nick, "key"); + keyfile = getConfigFile(); + escaped_nick = escape_nickname(nick); + value = get_nick_value(keyfile, escaped_nick, "key"); + key_mode = get_nick_value(keyfile, escaped_nick, "mode"); g_key_file_free(keyfile); g_free(escaped_nick); + /* Determine cipher mode */ + *mode = FISH_ECB_MODE; + if (key_mode) { + if (*key_mode == '1') + *mode = FISH_ECB_MODE; + else if (*key_mode == '2') + *mode = FISH_CBC_MODE; + g_free(key_mode); + } + if (!value) return NULL; - - if (strncmp(value, "+OK ", 4) != 0) { - /* Key is stored in plaintext */ - return value; - } else { + + if (strncmp(value, "+OK ", 4) == 0) { /* Key is encrypted */ - const char *encrypted = value+4; - const char *password = get_keystore_password(); - char *decrypted = fish_decrypt(password, strlen(password), encrypted); + encrypted = (char *) value; + encrypted += 4; + + encrypted_mode = FISH_ECB_MODE; + + if (*encrypted == '*') { + ++encrypted; + encrypted_mode = FISH_CBC_MODE; + } + + password = (char *) get_keystore_password(); + decrypted = fish_decrypt_str((const char *) password, strlen(password), (const char *) encrypted, encrypted_mode); g_free(value); return decrypted; + } else { + /* Key is stored in plaintext */ + return value; } } @@ -189,7 +217,7 @@ G_GNUC_END_IGNORE_DEPRECATIONS /** * Sets a key in the key store file. */ -gboolean keystore_store_key(const char *nick, const char *key) { +gboolean keystore_store_key(const char *nick, const char *key, enum fish_mode mode) { const char *password; char *encrypted; char *wrapped; @@ -204,11 +232,11 @@ gboolean keystore_store_key(const char *nick, const char *key) { password = get_keystore_password(); if (password) { /* Encrypt the password */ - encrypted = fish_encrypt(password, strlen(password), key); + encrypted = fish_encrypt(password, strlen(password), key, strlen(key), FISH_CBC_MODE); if (!encrypted) goto end; /* Prepend "+OK " */ - wrapped = g_strconcat("+OK ", encrypted, NULL); + wrapped = g_strconcat("+OK *", encrypted, NULL); g_free(encrypted); /* Store encrypted in file */ @@ -218,6 +246,9 @@ gboolean keystore_store_key(const char *nick, const char *key) { /* Store unencrypted in file */ g_key_file_set_string(keyfile, escaped_nick, "key", key); } + + /* Store cipher mode */ + g_key_file_set_integer(keyfile, escaped_nick, "mode", mode); /* Save key store file */ ok = save_keystore(keyfile); diff --git a/plugins/fishlim/keystore.h b/plugins/fishlim/keystore.h index 3d90606a..4af46693 100644 --- a/plugins/fishlim/keystore.h +++ b/plugins/fishlim/keystore.h @@ -28,9 +28,10 @@ #include #include +#include "fish.h" -char *keystore_get_key(const char *nick); -gboolean keystore_store_key(const char *nick, const char *key); +char *keystore_get_key(const char *nick, enum fish_mode *mode); +gboolean keystore_store_key(const char *nick, const char *key, enum fish_mode mode); gboolean keystore_delete_nick(const char *nick); #endif diff --git a/plugins/fishlim/meson.build b/plugins/fishlim/meson.build index 894923aa..06cf5f9a 100644 --- a/plugins/fishlim/meson.build +++ b/plugins/fishlim/meson.build @@ -2,6 +2,9 @@ if not libssl_dep.found() error('fish plugin requires openssl') endif +# Run tests +subdir('tests') + fishlim_sources = [ 'dh1080.c', 'fish.c', diff --git a/plugins/fishlim/plugin_hexchat.c b/plugins/fishlim/plugin_hexchat.c index 3eaad986..ddb692da 100644 --- a/plugins/fishlim/plugin_hexchat.c +++ b/plugins/fishlim/plugin_hexchat.c @@ -2,6 +2,7 @@ Copyright (c) 2010-2011 Samuel Lidén Borell Copyright (c) 2015 + Copyright (c) 2019 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -30,19 +31,20 @@ #include #include "hexchat-plugin.h" -#define HEXCHAT_MAX_WORDS 32 #include "fish.h" #include "dh1080.h" #include "keystore.h" #include "irc.h" +static const char *fish_modes[] = {"", "ECB", "CBC", NULL}; + static const char plugin_name[] = "FiSHLiM"; static const char plugin_desc[] = "Encryption plugin for the FiSH protocol. Less is More!"; -static const char plugin_version[] = "0.1.0"; +static const char plugin_version[] = "1.0.0"; -static const char usage_setkey[] = "Usage: SETKEY [] , sets the key for a channel or nick"; -static const char usage_delkey[] = "Usage: DELKEY , deletes the key for a channel or nick"; +static const char usage_setkey[] = "Usage: SETKEY [] [:], sets the key for a channel or nick. Modes: ECB, CBC"; +static const char usage_delkey[] = "Usage: DELKEY [], deletes the key for a channel or nick"; static const char usage_keyx[] = "Usage: KEYX [], performs DH1080 key-exchange with "; static const char usage_topic[] = "Usage: TOPIC+ , sets a new encrypted topic for the current channel"; static const char usage_notice[] = "Usage: NOTICE+ "; @@ -53,6 +55,13 @@ static hexchat_plugin *ph; static GHashTable *pending_exchanges; +/** + * Compare two nicks using the current plugin + */ +int irc_nick_cmp(const char *a, const char *b) { + return hexchat_nickcmp (ph, a, b); +} + /** * Returns the path to the key store file. */ @@ -88,7 +97,7 @@ static hexchat_context *find_context_on_network (const char *name) { int chan_id = hexchat_list_int(ph, channels, "id"); const char *chan_name = hexchat_list_str(ph, channels, "channel"); - if (chan_id == id && chan_name && hexchat_nickcmp (ph, chan_name, name) == 0) { + if (chan_id == id && chan_name && irc_nick_cmp (chan_name, name) == 0) { ret = (hexchat_context*)hexchat_list_str(ph, channels, "context"); break; } @@ -98,8 +107,109 @@ static hexchat_context *find_context_on_network (const char *name) { return ret; } -int irc_nick_cmp(const char *a, const char *b) { - return hexchat_nickcmp (ph, a, b); +/** + * Retrive the prefix character for own nick in current context + * @return @ or + or NULL + */ +char *get_my_own_prefix(void) { + char *result = NULL; + const char *own_nick; + hexchat_list *list; + + /* Display message */ + own_nick = hexchat_get_info(ph, "nick"); + + if (!own_nick) + return NULL; + + /* Get prefix for own nick if any */ + list = hexchat_list_get (ph, "users"); + if (list) { + while (hexchat_list_next(ph, list)) { + if (irc_nick_cmp(own_nick, hexchat_list_str(ph, list, "nick")) == 0) + result = g_strdup(hexchat_list_str(ph, list, "prefix")); + } + hexchat_list_free(ph, list); + } + + return result; +} + +/** + * Try to decrypt the first occurrence of fish message + * + * @param message Message to decrypt + * @param key Key of message + * @return Array of char with decrypted message or NULL. The returned string + * should be freed with g_free() when no longer needed. + */ +char *decrypt_raw_message(const char *message, const char *key) { + const char *prefixes[] = {"+OK ", "mcps ", NULL}; + char *start = NULL, *end = NULL; + char *left = NULL, *right = NULL; + char *encrypted = NULL, *decrypted = NULL; + int length = 0; + int index_prefix; + enum fish_mode mode; + GString *message_decrypted; + char *result = NULL; + + if (message == NULL || key == NULL) + return NULL; + + for (index_prefix = 0; index_prefix < 2; index_prefix++) { + start = g_strstr_len(message, strlen(message), prefixes[index_prefix]); + if (start) { + /* Length ALWAYS will be less that original message + * add '[CBC] ' length */ + message_decrypted = g_string_sized_new(strlen(message) + 6); + + /* Left part of message */ + left = g_strndup(message, start - message); + g_string_append(message_decrypted, left); + g_free(left); + + /* Encrypted part */ + start += strlen(prefixes[index_prefix]); + end = g_strstr_len(start, strlen(message), " "); + if (end) { + length = end - start; + right = end; + } + + if (length > 0) { + encrypted = g_strndup(start, length); + } else { + encrypted = g_strdup(start); + } + decrypted = fish_decrypt_from_nick(key, encrypted, &mode); + g_free(encrypted); + + if (decrypted == NULL) { + g_string_free(message_decrypted, TRUE); + return NULL; + } + + /* Add encrypted flag */ + g_string_append(message_decrypted, "["); + g_string_append(message_decrypted, fish_modes[mode]); + g_string_append(message_decrypted, "] "); + /* Decrypted message */ + g_string_append(message_decrypted, decrypted); + g_free(decrypted); + + /* Right part of message */ + if (right) { + g_string_append(message_decrypted, right); + } + + result = message_decrypted->str; + g_string_free(message_decrypted, FALSE); + return result; + } + } + + return NULL; } /*static int handle_debug(char *word[], char *word_eol[], void *userdata) { @@ -115,15 +225,24 @@ int irc_nick_cmp(const char *a, const char *b) { * Called when a message is to be sent. */ static int handle_outgoing(char *word[], char *word_eol[], void *userdata) { - const char *own_nick; + char *prefix; + enum fish_mode mode; + char *message; /* Encrypt the message if possible */ const char *channel = hexchat_get_info(ph, "channel"); - char *encrypted = fish_encrypt_for_nick(channel, word_eol[1]); + char *encrypted = fish_encrypt_for_nick(channel, word_eol[1], &mode); if (!encrypted) return HEXCHAT_EAT_NONE; + /* Get prefix for own nick if any */ + prefix = get_my_own_prefix(); + + /* Add encrypted flag */ + message = g_strconcat("[", fish_modes[mode], "] ", word_eol[1], NULL); + /* Display message */ - own_nick = hexchat_get_info(ph, "nick"); - hexchat_emit_print(ph, "Your Message", own_nick, word_eol[1], NULL); + hexchat_emit_print(ph, "Your Message", hexchat_get_info(ph, "nick"), message, prefix, NULL); + g_free(prefix); + g_free(message); /* Send message */ hexchat_commandf(ph, "PRIVMSG %s :+OK %s", channel, encrypted); @@ -139,96 +258,59 @@ static int handle_incoming(char *word[], char *word_eol[], hexchat_event_attrs * const char *prefix; const char *command; const char *recipient; - const char *encrypted; - const char *peice; + const char *raw_message = word_eol[1]; char *sender_nick; char *decrypted; - size_t w; - size_t ew; - size_t uw; - char prefix_char = 0; + size_t parameters_offset; GString *message; - if (!irc_parse_message((const char **)word, &prefix, &command, &w)) + if (!irc_parse_message((const char **)word, &prefix, &command, ¶meters_offset)) return HEXCHAT_EAT_NONE; - + /* Topic (command 332) has an extra parameter */ - if (!strcmp(command, "332")) w++; - - /* Look for encrypted data */ - for (ew = w+1; ew < HEXCHAT_MAX_WORDS-1; ew++) { - const char *s = (ew == w+1 ? word[ew]+1 : word[ew]); - if (*s && (s[1] == '+' || s[1] == 'm')) { prefix_char = *(s++); } - else { prefix_char = 0; } - if (strcmp(s, "+OK") == 0 || strcmp(s, "mcps") == 0) goto has_encrypted_data; + if (!strcmp(command, "332")) + parameters_offset++; + + /* Extract sender nick and recipient nick/channel and try to decrypt */ + recipient = word[parameters_offset]; + decrypted = decrypt_raw_message(raw_message, recipient); + if (decrypted == NULL) { + sender_nick = irc_prefix_get_nick(prefix); + decrypted = decrypt_raw_message(raw_message, sender_nick); + g_free(sender_nick); } - return HEXCHAT_EAT_NONE; - has_encrypted_data: ; - /* Extract sender nick and recipient nick/channel */ - sender_nick = irc_prefix_get_nick(prefix); - recipient = word[w]; - - /* Try to decrypt with these (the keys are searched for in the key store) */ - encrypted = word[ew+1]; - decrypted = fish_decrypt_from_nick(recipient, encrypted); - if (!decrypted) decrypted = fish_decrypt_from_nick(sender_nick, encrypted); - - /* Check for error */ - if (!decrypted) goto decrypt_error; - - /* Build unecrypted message */ - message = g_string_sized_new (100); /* TODO: more accurate estimation of size */ - g_string_append (message, "RECV"); + + /* Nothing to decrypt */ + if (decrypted == NULL) + return HEXCHAT_EAT_NONE; + + /* Build decrypted message */ + + /* decrypted + 'RECV ' + '@time=YYYY-MM-DDTHH:MM:SS.fffffZ ' */ + message = g_string_sized_new (strlen(decrypted) + 5 + 33); + g_string_append (message, "RECV "); if (attrs->server_time_utc) { GTimeVal tv = { (glong)attrs->server_time_utc, 0 }; char *timestamp = g_time_val_to_iso8601 (&tv); - g_string_append (message, " @time="); + g_string_append (message, "@time="); g_string_append (message, timestamp); + g_string_append (message, " "); g_free (timestamp); } - for (uw = 1; uw < HEXCHAT_MAX_WORDS; uw++) { - if (word[uw][0] != '\0') - g_string_append_c (message, ' '); - - if (uw == ew) { - /* Add the encrypted data */ - peice = decrypted; - uw++; /* Skip "OK+" */ - - if (ew == w+1) { - /* Prefix with colon, which gets stripped out otherwise */ - g_string_append_c (message, ':'); - } - - if (prefix_char) { - g_string_append_c (message, prefix_char); - } - - } else { - /* Add unencrypted data (for example, a prefix from a bouncer or bot) */ - peice = word[uw]; - } - - g_string_append (message, peice); - } + g_string_append (message, decrypted); g_free(decrypted); - /* Simulate unencrypted message */ - /* hexchat_printf(ph, "simulating: %s\n", message->str); */ + /* Fake server message + * RECV command will throw this function again, if message have multiple + * encrypted data, we will decrypt all */ hexchat_command(ph, message->str); - g_string_free (message, TRUE); - g_free(sender_nick); - return HEXCHAT_EAT_HEXCHAT; - decrypt_error: - g_free(decrypted); - g_free(sender_nick); - return HEXCHAT_EAT_NONE; + return HEXCHAT_EAT_HEXCHAT; } static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) { @@ -236,8 +318,8 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) { const char *dh_pubkey = word[5]; hexchat_context *query_ctx; const char *prefix; - gboolean cbc; char *sender, *secret_key, *priv_key = NULL; + enum fish_mode mode = FISH_ECB_MODE; if (!*dh_message || !*dh_pubkey || strlen(dh_pubkey) != 181) return HEXCHAT_EAT_NONE; @@ -248,25 +330,21 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) { sender = irc_prefix_get_nick(prefix); query_ctx = find_context_on_network(sender); if (query_ctx) - hexchat_set_context(ph, query_ctx); + g_assert(hexchat_set_context(ph, query_ctx) == 1); dh_message++; /* : prefix */ if (*dh_message == '+' || *dh_message == '-') dh_message++; /* identify-msg */ - cbc = g_strcmp0 (word[6], "CBC") == 0; + if (g_strcmp0 (word[6], "CBC") == 0) + mode = FISH_CBC_MODE; if (!strcmp(dh_message, "DH1080_INIT")) { char *pub_key; - if (cbc) { - hexchat_print(ph, "Received key exchange for CBC mode which is not supported."); - goto cleanup; - } - - hexchat_printf(ph, "Received public key from %s, sending mine...", sender); + hexchat_printf(ph, "Received public key from %s (%s), sending mine...", sender, fish_modes[mode]); if (dh1080_generate_key(&priv_key, &pub_key)) { - hexchat_commandf(ph, "quote NOTICE %s :DH1080_FINISH %s", sender, pub_key); + hexchat_commandf(ph, "quote NOTICE %s :DH1080_FINISH %s%s", sender, pub_key, (mode == FISH_CBC_MODE) ? " CBC" : ""); g_free(pub_key); } else { hexchat_print(ph, "Failed to generate keys"); @@ -279,11 +357,6 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) { g_hash_table_steal(pending_exchanges, sender_lower); g_free(sender_lower); - if (cbc) { - hexchat_print(ph, "Received key exchange for CBC mode which is not supported."); - goto cleanup; - } - if (!priv_key) { hexchat_printf(ph, "Received a key exchange response for unknown user: %s", sender); goto cleanup; @@ -295,8 +368,8 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) { } if (dh1080_compute_key(priv_key, dh_pubkey, &secret_key)) { - keystore_store_key(sender, secret_key); - hexchat_printf(ph, "Stored new key for %s", sender); + keystore_store_key(sender, secret_key, mode); + hexchat_printf(ph, "Stored new key for %s (%s)", sender, fish_modes[mode]); g_free(secret_key); } else { hexchat_print(ph, "Failed to create secret key!"); @@ -314,6 +387,7 @@ cleanup: static int handle_setkey(char *word[], char *word_eol[], void *userdata) { const char *nick; const char *key; + enum fish_mode mode; /* Check syntax */ if (*word[2] == '\0') { @@ -331,9 +405,17 @@ static int handle_setkey(char *word[], char *word_eol[], void *userdata) { key = word_eol[3]; } + mode = FISH_ECB_MODE; + if (g_ascii_strncasecmp("cbc:", key, 4) == 0) { + key = key+4; + mode = FISH_CBC_MODE; + } else if (g_ascii_strncasecmp("ecb:", key, 4) == 0) { + key = key+4; + } + /* Set password */ - if (keystore_store_key(nick, key)) { - hexchat_printf(ph, "Stored key for %s\n", nick); + if (keystore_store_key(nick, key, mode)) { + hexchat_printf(ph, "Stored key for %s (%s)\n", nick, fish_modes[mode]); } else { hexchat_printf(ph, "\00305Failed to store key in addon_fishlim.conf\n"); } @@ -345,22 +427,30 @@ static int handle_setkey(char *word[], char *word_eol[], void *userdata) { * Command handler for /delkey */ static int handle_delkey(char *word[], char *word_eol[], void *userdata) { - const char *nick; + char *nick = NULL; + int ctx_type = 0; - /* Check syntax */ - if (*word[2] == '\0' || *word[3] != '\0') { - hexchat_printf(ph, "%s\n", usage_delkey); - return HEXCHAT_EAT_HEXCHAT; + /* Delete key from input */ + if (*word[2] != '\0') { + nick = g_strstrip(g_strdup(word_eol[2])); + } else { /* Delete key from current context */ + nick = g_strdup(hexchat_get_info(ph, "channel")); + ctx_type = hexchat_list_int(ph, NULL, "type"); + + /* Only allow channel or dialog */ + if (ctx_type < 2 || ctx_type > 3) { + hexchat_printf(ph, "%s\n", usage_delkey); + return HEXCHAT_EAT_HEXCHAT; + } } - nick = g_strstrip (word_eol[2]); - /* Delete the given nick from the key store */ if (keystore_delete_nick(nick)) { hexchat_printf(ph, "Deleted key for %s\n", nick); } else { hexchat_printf(ph, "\00305Failed to delete key in addon_fishlim.conf!\n"); } + g_free(nick); return HEXCHAT_EAT_HEXCHAT; } @@ -379,7 +469,7 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) { } if (query_ctx) { - hexchat_set_context(ph, query_ctx); + g_assert(hexchat_set_context(ph, query_ctx) == 1); ctx_type = hexchat_list_int(ph, NULL, "type"); } @@ -391,8 +481,8 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) { if (dh1080_generate_key(&priv_key, &pub_key)) { g_hash_table_replace (pending_exchanges, g_ascii_strdown(target, -1), priv_key); - hexchat_commandf(ph, "quote NOTICE %s :DH1080_INIT %s", target, pub_key); - hexchat_printf(ph, "Sent public key to %s, waiting for reply...", target); + hexchat_commandf(ph, "quote NOTICE %s :DH1080_INIT %s CBC", target, pub_key); + hexchat_printf(ph, "Sent public key to %s (CBC), waiting for reply...", target); g_free(pub_key); } else { @@ -409,6 +499,7 @@ static int handle_crypt_topic(char *word[], char *word_eol[], void *userdata) { const char *target; const char *topic = word_eol[2]; char *buf; + enum fish_mode mode; if (!*topic) { hexchat_print(ph, usage_topic); @@ -421,7 +512,7 @@ static int handle_crypt_topic(char *word[], char *word_eol[], void *userdata) { } target = hexchat_get_info(ph, "channel"); - buf = fish_encrypt_for_nick(target, topic); + buf = fish_encrypt_for_nick(target, topic, &mode); if (buf == NULL) { hexchat_printf(ph, "/topic+ error, no key found for %s", target); return HEXCHAT_EAT_ALL; @@ -439,21 +530,25 @@ static int handle_crypt_notice(char *word[], char *word_eol[], void *userdata) { const char *target = word[2]; const char *notice = word_eol[3]; + char *notice_flag; char *buf; + enum fish_mode mode; if (!*target || !*notice) { hexchat_print(ph, usage_notice); return HEXCHAT_EAT_ALL; } - buf = fish_encrypt_for_nick(target, notice); + buf = fish_encrypt_for_nick(target, notice, &mode); if (buf == NULL) { hexchat_printf(ph, "/notice+ error, no key found for %s.", target); return HEXCHAT_EAT_ALL; } hexchat_commandf(ph, "quote NOTICE %s :+OK %s", target, buf); - hexchat_emit_print(ph, "Notice Sent", target, notice); + notice_flag = g_strconcat("[", fish_modes[mode], "] ", notice, NULL); + hexchat_emit_print(ph, "Notice Send", target, notice_flag); + g_free(notice_flag); g_free(buf); return HEXCHAT_EAT_ALL; @@ -462,19 +557,21 @@ static int handle_crypt_notice(char *word[], char *word_eol[], void *userdata) /** * Command handler for /msg+ */ -static int handle_crypt_msg(char *word[], char *word_eol[], void *userdata) -{ +static int handle_crypt_msg(char *word[], char *word_eol[], void *userdata) { const char *target = word[2]; const char *message = word_eol[3]; + char *message_flag; + char *prefix; hexchat_context *query_ctx; char *buf; + enum fish_mode mode; if (!*target || !*message) { hexchat_print(ph, usage_msg); return HEXCHAT_EAT_ALL; } - buf = fish_encrypt_for_nick(target, message); + buf = fish_encrypt_for_nick(target, message, &mode); if (buf == NULL) { hexchat_printf(ph, "/msg+ error, no key found for %s", target); return HEXCHAT_EAT_ALL; @@ -484,13 +581,17 @@ static int handle_crypt_msg(char *word[], char *word_eol[], void *userdata) query_ctx = find_context_on_network(target); if (query_ctx) { - hexchat_set_context(ph, query_ctx); + g_assert(hexchat_set_context(ph, query_ctx) == 1); - /* FIXME: Mode char */ + prefix = get_my_own_prefix(); + + /* Add encrypted flag */ + message_flag = g_strconcat("[", fish_modes[mode], "] ", message, NULL); hexchat_emit_print(ph, "Your Message", hexchat_get_info(ph, "nick"), - message, "", ""); - } - else { + message_flag, prefix, NULL); + g_free(prefix); + g_free(message_flag); + } else { hexchat_emit_print(ph, "Message Send", target, message); } @@ -501,8 +602,9 @@ static int handle_crypt_msg(char *word[], char *word_eol[], void *userdata) static int handle_crypt_me(char *word[], char *word_eol[], void *userdata) { const char *channel = hexchat_get_info(ph, "channel"); char *buf; + enum fish_mode mode; - buf = fish_encrypt_for_nick(channel, word_eol[2]); + buf = fish_encrypt_for_nick(channel, word_eol[2], &mode); if (!buf) return HEXCHAT_EAT_NONE; diff --git a/plugins/fishlim/tests/fake/keystore.c b/plugins/fishlim/tests/fake/keystore.c new file mode 100644 index 00000000..855b3451 --- /dev/null +++ b/plugins/fishlim/tests/fake/keystore.c @@ -0,0 +1,47 @@ +/* + + Copyright (c) 2010 Samuel Lidén Borell + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +#include "../../fish.h" + + +/** + * Extracts a key from the key store file. + */ +char *keystore_get_key(const char *nick, enum fish_mode *mode) { + return NULL; +} + +/** + * Sets a key in the key store file. + */ +gboolean keystore_store_key(const char *nick, const char *key, enum fish_mode mode) { + return NULL; +} + +/** + * Deletes a nick from the key store. + */ +gboolean keystore_delete_nick(const char *nick) { + return NULL; +} diff --git a/plugins/fishlim/tests/meson.build b/plugins/fishlim/tests/meson.build new file mode 100644 index 00000000..3b75018e --- /dev/null +++ b/plugins/fishlim/tests/meson.build @@ -0,0 +1,17 @@ +subdir('old_version') + +fishlim_test_sources = [ + 'tests.c', + 'fake/keystore.c', + '../fish.c', + '../utils.c', +] + +fishlim_tests = executable('fishlim_tests', fishlim_test_sources, + dependencies: [libgio_dep, libssl_dep], + link_with : fishlim_old_lib +) + +test('Fishlim Tests', fishlim_tests, + timeout: 90 +) diff --git a/plugins/fishlim/tests/old_version/fish.c b/plugins/fishlim/tests/old_version/fish.c new file mode 100644 index 00000000..ff0b8fea --- /dev/null +++ b/plugins/fishlim/tests/old_version/fish.c @@ -0,0 +1,194 @@ +/* + + Copyright (c) 2010 Samuel Lidén Borell + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +#ifdef __APPLE__ +#define __AVAILABILITYMACROS__ +#define DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER +#endif + +#include +#include +#include + +#include "fish.h" + +#define IB 64 +static const char fish_base64[64] = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +static const signed char fish_unbase64[256] = { + IB,IB,IB,IB,IB,IB,IB,IB, IB,IB,IB,IB,IB,IB,IB,IB, + IB,IB,IB,IB,IB,IB,IB,IB, IB,IB,IB,IB,IB,IB,IB,IB, +/* ! " # $ % & ' ( ) * + , - . / */ + IB,IB,IB,IB,IB,IB,IB,IB, IB,IB,IB,IB,IB,IB, 0, 1, +/* 0 1 2 3 4 5 6 7 8 9 : ; < = > ? */ + 2, 3, 4, 5, 6, 7, 8, 9, 10,11,IB,IB,IB,IB,IB,IB, +/* @ A B C D E F G H I J K L M N O */ + IB,38,39,40,41,42,43,44, 45,46,47,48,49,50,51,52, +/* P Q R S T U V W X Y Z [ \ ] ^ _*/ + 53,54,55,56,57,58,59,60, 61,62,63,IB,IB,IB,IB,IB, +/* ` a b c d e f g h i j k l m n o */ + IB,12,13,14,15,16,17,18, 19,20,21,22,23,24,25,26, +/* p q r s t u v w x y z { | } ~ */ + 27,28,29,30,31,32,33,34, 35,36,37,IB,IB,IB,IB,IB, +}; + +#define GET_BYTES(dest, source) do { \ + *((dest)++) = ((source) >> 24) & 0xFF; \ + *((dest)++) = ((source) >> 16) & 0xFF; \ + *((dest)++) = ((source) >> 8) & 0xFF; \ + *((dest)++) = (source) & 0xFF; \ +} while (0); + + +char *__old_fish_encrypt(const char *key, size_t keylen, const char *message) { + BF_KEY bfkey; + size_t messagelen; + size_t i; + int j; + char *encrypted; + char *end; + unsigned char bit; + unsigned char word; + unsigned char d; + BF_set_key(&bfkey, keylen, (const unsigned char*)key); + + messagelen = strlen(message); + if (messagelen == 0) return NULL; + encrypted = g_malloc(((messagelen - 1) / 8) * 12 + 12 + 1); /* each 8-byte block becomes 12 bytes */ + end = encrypted; + + while (*message) { + /* Read 8 bytes (a Blowfish block) */ + BF_LONG binary[2] = { 0, 0 }; + unsigned char c; + for (i = 0; i < 8; i++) { + c = message[i]; + binary[i >> 2] |= c << 8*(3 - (i&3)); + if (c == '\0') break; + } + message += 8; + + /* Encrypt block */ + BF_encrypt(binary, &bfkey); + + /* Emit FiSH-BASE64 */ + bit = 0; + word = 1; + for (j = 0; j < 12; j++) { + d = fish_base64[(binary[word] >> bit) & 63]; + *(end++) = d; + bit += 6; + if (j == 5) { + bit = 0; + word = 0; + } + } + + /* Stop if a null terminator was found */ + if (c == '\0') break; + } + *end = '\0'; + return encrypted; +} + + +char *__old_fish_decrypt(const char *key, size_t keylen, const char *data) { + BF_KEY bfkey; + size_t i; + char *decrypted; + char *end; + unsigned char bit; + unsigned char word; + unsigned char d; + BF_set_key(&bfkey, keylen, (const unsigned char*)key); + + decrypted = g_malloc(strlen(data) + 1); + end = decrypted; + + while (*data) { + /* Convert from FiSH-BASE64 */ + BF_LONG binary[2] = { 0, 0 }; + bit = 0; + word = 1; + for (i = 0; i < 12; i++) { + d = fish_unbase64[(const unsigned char)*(data++)]; + if (d == IB) goto decrypt_end; + binary[word] |= (unsigned long)d << bit; + bit += 6; + if (i == 5) { + bit = 0; + word = 0; + } + } + + /* Decrypt block */ + BF_decrypt(binary, &bfkey); + + /* Copy to buffer */ + GET_BYTES(end, binary[0]); + GET_BYTES(end, binary[1]); + } + + decrypt_end: + *end = '\0'; + return decrypted; +} + +/** + * Encrypts a message (see __old_fish_decrypt). The key is searched for in the + * key store. + */ +char *__old_fish_encrypt_for_nick(const char *nick, const char *data) { + char *key; + char *encrypted; + + /* Look for key */ + /*key = keystore_get_key(nick);*/ + if (!key) return NULL; + + /* Encrypt */ + encrypted = __old_fish_encrypt(key, strlen(key), data); + + g_free(key); + return encrypted; +} + +/** + * Decrypts a message (see __old_fish_decrypt). The key is searched for in the + * key store. + */ +char *__old_fish_decrypt_from_nick(const char *nick, const char *data) { + char *key; + char *decrypted; + /* Look for key */ + /*key = keystore_get_key(nick);*/ + if (!key) return NULL; + + /* Decrypt */ + decrypted = __old_fish_decrypt(key, strlen(key), data); + + g_free(key); + return decrypted; +} + + diff --git a/plugins/fishlim/tests/old_version/fish.h b/plugins/fishlim/tests/old_version/fish.h new file mode 100644 index 00000000..b4c66572 --- /dev/null +++ b/plugins/fishlim/tests/old_version/fish.h @@ -0,0 +1,39 @@ +/* + + Copyright (c) 2010 Samuel Lidén Borell + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +#ifndef FISH_OLD_H +#define FISH_OLD_H + +#include + +#include + +char *__old_fish_encrypt(const char *key, size_t keylen, const char *message); +char *__old_fish_decrypt(const char *key, size_t keylen, const char *data); +char *__old_fish_encrypt_for_nick(const char *nick, const char *data); +char *__old_fish_decrypt_from_nick(const char *nick, const char *data); + +#endif + + diff --git a/plugins/fishlim/tests/old_version/meson.build b/plugins/fishlim/tests/old_version/meson.build new file mode 100644 index 00000000..54647d71 --- /dev/null +++ b/plugins/fishlim/tests/old_version/meson.build @@ -0,0 +1,4 @@ +fishlim_old_lib = shared_library('fishlim_old_lib', + ['fish.c'], + dependencies: [libgio_dep, libssl_dep], +) diff --git a/plugins/fishlim/tests/tests.c b/plugins/fishlim/tests/tests.c new file mode 100644 index 00000000..e34532bf --- /dev/null +++ b/plugins/fishlim/tests/tests.c @@ -0,0 +1,248 @@ +/* + + Copyright (c) 2020 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +#ifndef PLUGIN_HEXCHAT_FISHLIM_TEST_H +#define PLUGIN_HEXCHAT_FISHLIM_TEST_H + +// Libs +#include +#include +// Project Libs +#include "../fish.h" +#include "../utils.h" +#include "old_version/fish.h" + +/** + * Auxiliary function: Generate a random string + * @param out Preallocated string to fill + * @param len Size of bytes to fill + */ +void random_string(char *out, size_t len) { + GRand *rand = NULL; + int i = 0; + + rand = g_rand_new(); + for (i = 0; i < len; ++i) { + out[i] = g_rand_int_range(rand, 1, 256); + } + + out[len] = 0; + + g_rand_free(rand); +} + + +/** + * Check encrypt and decrypt in ECB mode and compare with old implementation + */ +void __ecb() { + char *bo64 = NULL, *b64 = NULL; + char *deo = NULL, *de = NULL; + int key_len, message_len = 0; + char key[57]; + char message[1000]; + + /* Generate key 32–448 bits (Yes, I start with 8 bits) */ + for (key_len = 1; key_len < 57; ++key_len) { + + random_string(key, key_len); + + for (message_len = 1; message_len < 1000; ++message_len) { + random_string(message, message_len); + + /* Encrypt */ + bo64 = __old_fish_encrypt(key, key_len, message); + g_assert_nonnull(bo64); + b64 = fish_encrypt(key, key_len, message, message_len, FISH_ECB_MODE); + g_assert_nonnull(b64); + g_assert_cmpuint(g_strcmp0(b64, bo64), == , 0); + + /* Decrypt */ + /* Linear */ + deo = __old_fish_decrypt(key, key_len, bo64); + de = fish_decrypt_str(key, key_len, b64, FISH_ECB_MODE); + g_assert_nonnull(deo); + g_assert_nonnull(de); + g_assert_cmpuint(g_strcmp0(de, message), == , 0); + g_assert_cmpuint(g_strcmp0(deo, message), == , 0); + g_assert_cmpuint(g_strcmp0(de, deo), == , 0); + g_free(deo); + g_free(de); + /* Mixed */ + deo = __old_fish_decrypt(key, key_len, b64); + de = fish_decrypt_str(key, key_len, bo64, FISH_ECB_MODE); + g_assert_nonnull(deo); + g_assert_nonnull(de); + g_assert_cmpuint(g_strcmp0(de, message), == , 0); + g_assert_cmpuint(g_strcmp0(deo, message), == , 0); + g_assert_cmpuint(g_strcmp0(de, deo), == , 0); + g_free(deo); + g_free(de); + + /* Free */ + g_free(bo64); + g_free(b64); + } + } +} + +/** + * Check encrypt and decrypt in CBC mode + */ +void __cbc() { + char *b64 = NULL; + char *de = NULL; + int key_len, message_len = 0; + char key[57]; + char message[1000]; + + /* Generate key 32–448 bits (Yes, I start with 8 bits) */ + for (key_len = 1; key_len < 57; ++key_len) { + + random_string(key, key_len); + + for (message_len = 1; message_len < 1000; ++message_len) { + random_string(message, message_len); + + /* Encrypt */ + b64 = fish_encrypt(key, key_len, message, message_len, FISH_CBC_MODE); + g_assert_nonnull(b64); + + /* Decrypt */ + /* Linear */ + de = fish_decrypt_str(key, key_len, b64, FISH_CBC_MODE); + g_assert_nonnull(de); + g_assert_cmpuint(g_strcmp0(de, message), == , 0); + g_free(de); + + /* Free */ + g_free(b64); + } + } +} + +/** + * Check the calculation of final length from an encoded string in Base64 + */ +void __base64_len() { + char *b64 = NULL; + int i, message_len = 0; + char message[1000]; + + for (i = 0; i < 10; ++i) { + + for (message_len = 1; message_len < 1000; ++message_len) { + random_string(message, message_len); + b64 = g_base64_encode((const unsigned char *) message, message_len); + g_assert_nonnull(b64); + g_assert_cmpuint(strlen(b64), == , base64_len(message_len)); + g_free(b64); + } + } +} + +/** + * Check the calculation of final length from an encoded string in BlowcryptBase64 + */ +void __base64_fish_len() { + char *b64 = NULL; + int i, message_len = 0; + char message[1000]; + + for (i = 0; i < 10; ++i) { + + for (message_len = 1; message_len < 1000; ++message_len) { + random_string(message, message_len); + b64 = fish_base64_encode(message, message_len); + g_assert_nonnull(b64); + g_assert_cmpuint(strlen(b64), == , base64_fish_len(message_len)); + g_free(b64); + } + } +} + + +/** + * Check the calculation of final length from an encrypted string in ECB mode + */ +void __base64_ecb_len() { + char *b64 = NULL; + int key_len, message_len = 0; + char key[57]; + char message[1000]; + + /* Generate key 32–448 bits (Yes, I start with 8 bits) */ + for (key_len = 1; key_len < 57; ++key_len) { + + random_string(key, key_len); + + for (message_len = 1; message_len < 1000; ++message_len) { + random_string(message, message_len); + b64 = fish_encrypt(key, key_len, message, message_len, FISH_ECB_MODE); + g_assert_nonnull(b64); + g_assert_cmpuint(strlen(b64), == , ecb_len(message_len)); + g_free(b64); + } + } +} + +/** + * Check the calculation of final length from an encrypted string in CBC mode + */ +void __base64_cbc_len() { + char *b64 = NULL; + int key_len, message_len = 0; + char key[57]; + char message[1000]; + + /* Generate key 32–448 bits (Yes, I start with 8 bits) */ + for (key_len = 1; key_len < 57; ++key_len) { + + random_string(key, key_len); + + for (message_len = 1; message_len < 1000; ++message_len) { + random_string(message, message_len); + b64 = fish_encrypt(key, key_len, message, message_len, FISH_CBC_MODE); + g_assert_nonnull(b64); + g_assert_cmpuint(strlen(b64), == , cbc_len(message_len)); + g_free(b64); + } + } +} + +int main(int argc, char *argv[]) { + + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/fishlim/__ecb", __ecb); + g_test_add_func("/fishlim/__cbc", __ecb); + g_test_add_func("/fishlim/__base64_len", __base64_len); + g_test_add_func("/fishlim/__base64_fish_len", __base64_fish_len); + g_test_add_func("/fishlim/__base64_ecb_len", __base64_ecb_len); + g_test_add_func("/fishlim/__base64_cbc_len", __base64_cbc_len); + + return g_test_run(); +} + +#endif //PLUGIN_HEXCHAT_FISHLIM_TEST_H \ No newline at end of file diff --git a/plugins/fishlim/utils.c b/plugins/fishlim/utils.c new file mode 100644 index 00000000..5af87404 --- /dev/null +++ b/plugins/fishlim/utils.c @@ -0,0 +1,70 @@ +/* + + Copyright (c) 2020 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +#include "utils.h" + +/** + * Calculate the length of Base64-encoded string + * + * @param plaintext_len Size of clear text to encode + * @return Size of encoded string + */ +unsigned long base64_len(size_t plaintext_len) { + int length_unpadded = (4 * plaintext_len) / 3; + /* Add padding */ + return length_unpadded % 4 != 0 ? length_unpadded + (4 - length_unpadded % 4) : length_unpadded; +} + +/** + * Calculate the length of BlowcryptBase64-encoded string + * + * @param plaintext_len Size of clear text to encode + * @return Size of encoded string + */ +unsigned long base64_fish_len(size_t plaintext_len) { + int length_unpadded = (12 * plaintext_len) / 8; + /* Add padding */ + return length_unpadded % 12 != 0 ? length_unpadded + (12 - length_unpadded % 12) : length_unpadded; +} + +/** + * Calculate the length of fish-encrypted string in CBC mode + * + * @param plaintext_len Size of clear text to encode + * @return Size of encoded string + */ +unsigned long cbc_len(size_t plaintext_len) { + /*IV + DATA + Zero Padding */ + return base64_len(8 + (plaintext_len % 8 != 0 ? plaintext_len + 8 - (plaintext_len % 8) : plaintext_len)); +} + +/** + * Calculate the length of fish-encrypted string in ECB mode + * + * @param plaintext_len Size of clear text to encode + * @return Size of encoded string + */ +unsigned long ecb_len(size_t plaintext_len) { + return base64_fish_len(plaintext_len); +} \ No newline at end of file diff --git a/plugins/fishlim/utils.h b/plugins/fishlim/utils.h new file mode 100644 index 00000000..9e2b3355 --- /dev/null +++ b/plugins/fishlim/utils.h @@ -0,0 +1,35 @@ +/* + + Copyright (c) 2020 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +#ifndef PLUGIN_HEXCHAT_FISHLIM_UTILS_H +#define PLUGIN_HEXCHAT_FISHLIM_UTILS_H + +#include + +unsigned long base64_len(size_t plaintext_len); +unsigned long base64_fish_len(size_t plaintext_len); +unsigned long cbc_len(size_t plaintext_len); +unsigned long ecb_len(size_t plaintext_len); + +#endif \ No newline at end of file