diff options
author | Samuel Lidén Borell <samuel@kodafritt.se> | 2015-06-21 14:13:49 +0200 |
---|---|---|
committer | Samuel Lidén Borell <samuel@kodafritt.se> | 2015-06-21 14:13:49 +0200 |
commit | a0396e8e26799051c457d71c71472d4221f3c06e (patch) | |
tree | 79a923ced179c71eb17e402283482c14b374388b | |
parent | 0cbb5ec0949033fa7bdd4b2afe72d70c478042dc (diff) | |
download | fishlim-a0396e8e26799051c457d71c71472d4221f3c06e.tar.gz fishlim-a0396e8e26799051c457d71c71472d4221f3c06e.tar.bz2 fishlim-a0396e8e26799051c457d71c71472d4221f3c06e.zip |
Implement encryption in CBC mode
-rw-r--r-- | README | 15 | ||||
-rw-r--r-- | fish.c | 74 | ||||
-rw-r--r-- | fish.h | 3 | ||||
-rw-r--r-- | keystore.c | 48 | ||||
-rw-r--r-- | keystore.h | 7 | ||||
-rw-r--r-- | plugin_xchat.c | 63 | ||||
-rw-r--r-- | test.c | 61 |
7 files changed, 246 insertions, 25 deletions
@@ -20,6 +20,7 @@ Working: * Using unecrypted keys / keys without a password from blow.ini * Pure protocol-level filtering (works with highlighting, nick coloring etc) * Partially encrypted messages (i.e. prefixed with nickname by a bouncer) + * CBC mode Not working: * Key exchange @@ -32,10 +33,22 @@ Not working: Commands -------- -/setkey [nick or #channel] password +/setkey [nick or #channel] [mode:]password Sets the encryption key for the nick or channel to password. The keys are stored in the configuration file in ~/.config/hexchat/blow.ini + + Optionally, the block cipher mode for outgoing messages may be specified. + If unspecified it will be ECB for backwards compatibility, but for + greater security, please use CBC mode if possible. + + For incoming messages, the block cipher mode is auto-detected, regardless + of which mode was configured with this command. + + +/ciphermode [nick or #channel] mode + + Sets the cipher mode to ECB or CBC. See /setkey. /delkey nick-or-#channel @@ -27,6 +27,7 @@ #include <openssl/blowfish.h> #include <openssl/bio.h> #include <openssl/evp.h> +#include <openssl/rand.h> #include "keystore.h" #include "fish.h" @@ -58,9 +59,9 @@ static const signed char fish_unbase64[256] = { } while (0); /** - * Encrypts a message. Currently, only ECB mode is supported. + * Encrypts a message in ECB mode. */ -char *fish_encrypt(const char *key, size_t keylen, const char *message) { +char *fish_encrypt_ecb(const char *key, size_t keylen, const char *message) { BF_KEY bfkey; size_t messagelen; size_t i; @@ -113,9 +114,67 @@ char *fish_encrypt(const char *key, size_t keylen, const char *message) { } /** + * Encrypts a message in CBC mode. + */ +char *fish_encrypt_cbc(const char *key, size_t keylen, const char *message) { + BF_KEY bfkey; + unsigned char *encrypted = NULL; + BIO *b64 = NULL; + BF_set_key(&bfkey, keylen, (const unsigned char*)key); + + size_t messagelen = strlen(message); + if (messagelen == 0) goto err; + + // Allocate structure for final encrypted data. + int cryptlen = 8 + ((messagelen+7)&~7); + encrypted = malloc(cryptlen); + if (!encrypted) goto err; + + // Generate IV + unsigned char iv[8]; + RAND_pseudo_bytes(iv, 8); + memcpy(encrypted, iv, 8); + + // Encrypt in CBC mode. The IV is overwritten + BF_cbc_encrypt((const unsigned char*)message, encrypted+8, messagelen, &bfkey, iv, BF_ENCRYPT); + + // Base64 encode + b64 = BIO_new(BIO_f_base64()); + if (!b64) goto err; + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + BIO *bmem = BIO_new(BIO_s_mem()); + if (!bmem) goto err; + BIO *bio = BIO_push(b64, bmem); + if (cryptlen != 0) { + BIO_write(bio, encrypted, cryptlen); + } + free(encrypted); + encrypted = NULL; + BIO_flush(bio); + + unsigned char *b64enc; + int b64len = (int)BIO_ctrl(bio, BIO_CTRL_INFO, 0, (char *)&b64enc); + if (b64len <= 0) goto err; + + // Copy the string + char *encoded = malloc(b64len+2); + encoded[0] = '*'; // prepended to indicate CBC mode + memcpy(encoded+1, b64enc, b64len); + encoded[b64len+1] = '\0'; // null terminator + BIO_free_all(b64); // data has been copied now + return encoded; + + err: + free(encrypted); + BIO_free_all(b64); + return NULL; +} + + +/** * Decrypts a message in CBC mode. The leading "*" should be included. */ -char *fish_decrypt_cbc(const char *key, size_t keylen, const char *data) { +static char *fish_decrypt_cbc(const char *key, size_t keylen, const char *data) { BF_KEY bfkey; unsigned char *decrypted, *bindata = NULL; BIO *b64 = NULL; @@ -224,13 +283,16 @@ char *fish_decrypt(const char *key, size_t keylen, const char *data) { char *fish_encrypt_for_nick(const char *nick, const char *data) { char *key; char *encrypted; + bool is_cbc; // Look for key - key = keystore_get_key(nick); + key = keystore_get_key(nick, &is_cbc); if (!key) return NULL; // Encrypt - encrypted = fish_encrypt(key, strlen(key), data); + encrypted = is_cbc ? + fish_encrypt_cbc(key, strlen(key), data) : + fish_encrypt_ecb(key, strlen(key), data); free(key); return encrypted; @@ -244,7 +306,7 @@ char *fish_decrypt_from_nick(const char *nick, const char *data) { char *key; char *decrypted; // Look for key - key = keystore_get_key(nick); + key = keystore_get_key(nick, NULL); if (!key) return NULL; // Decrypt @@ -32,7 +32,8 @@ #endif #include <stddef.h> -char *fish_encrypt(const char *key, size_t keylen, const char *message); +char *fish_encrypt_ecb(const char *key, size_t keylen, const char *message); +char *fish_encrypt_cbc(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); @@ -1,6 +1,6 @@ /* - Copyright (c) 2010 Samuel Lidén Borell <samuel@kodafritt.se> + Copyright (c) 2010-2015 Samuel Lidén Borell <samuel@kodafritt.se> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -32,6 +32,7 @@ #include "plugin_xchat.h" +// TODO use CBC in keystore_store_key once this is implemented static char *keystore_password = NULL; @@ -86,13 +87,21 @@ 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, bool *is_cbc) { // Get the key GKeyFile *keyfile = getConfigFile(); gchar *value = get_nick_value(keyfile, nick, "key"); + gchar *cbcval = get_nick_value(keyfile, nick, "cbc"); + if (is_cbc) { + // All values except 0 means "use CBC". + // Mircryption parses "N" and "n" as false also. + *is_cbc = (cbcval && *cbcval && strcmp(cbcval, "0"); + } + g_free(cbcval); g_key_file_free(keyfile); if (!value) return NULL; + // TODO if the key value begins with cbc: or CBC: then it's also a CBC key if (strncmp(value, "+OK ", 4) != 0) { // Key is stored in plaintext return import_glib_string(value); @@ -101,6 +110,7 @@ char *keystore_get_key(const char *nick) { const char *encrypted = value+4; const char *password = get_keystore_password(); char *decrypted = fish_decrypt(password, strlen(password), encrypted); + secure_erase(value, strlen(value)); g_free(value); return decrypted; } @@ -147,7 +157,7 @@ static bool save_keystore(GKeyFile *keyfile) { /** * Sets a key in the key store file. */ -bool keystore_store_key(const char *nick, const char *key) { +bool keystore_store_key(const char *nick, const char *key, bool is_cbc) { const char *password; char *encrypted; char *wrapped; @@ -161,7 +171,8 @@ bool 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); + // TODO Should use CBC here once keystore_password is implemented + encrypted = fish_encrypt_ecb(password, strlen(password), key); if (!encrypted) goto end; // Prepend "+OK " @@ -176,6 +187,35 @@ bool keystore_store_key(const char *nick, const char *key) { g_key_file_set_string(keyfile, nick, "key", key); } + // Store the CBC status. Note that all keys for the nick are deleted above + if (is_cbc) { + g_key_file_set_string(keyfile, nick, "cbc", "1"); + } + + // Save key store file + ok = save_keystore(keyfile); + + end: + g_key_file_free(keyfile); + return ok; +} + +/** + * Sets the block cipher mode for a nick to ECB or CBC. + */ +bool keystore_set_mode(const char *nick, bool is_cbc) { + bool ok = false; + GKeyFile *keyfile = getConfigFile(); + + if (!g_key_file_has_group(keyfile, nick)) goto end; + + // Store the CBC status + if (is_cbc) { + g_key_file_set_string(keyfile, nick, "cbc", "1"); + } else { + g_key_file_remove_key(keyfile, nick, "cbc", NULL); + } + // Save key store file ok = save_keystore(keyfile); @@ -1,6 +1,6 @@ /* - Copyright (c) 2010 Samuel Lidén Borell <samuel@kodafritt.se> + Copyright (c) 2010-2015 Samuel Lidén Borell <samuel@kodafritt.se> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -32,8 +32,9 @@ #endif #include <stddef.h> -char *keystore_get_key(const char *nick); -bool keystore_store_key(const char *nick, const char *key); +char *keystore_get_key(const char *nick, bool *is_cbc); +bool keystore_store_key(const char *nick, const char *key, bool is_cbc); +bool keystore_set_mode(const char *nick, bool is_cbc); bool keystore_delete_nick(const char *nick); void keystore_secure_free(void *ptr, size_t size); diff --git a/plugin_xchat.c b/plugin_xchat.c index 95b9a26..8e6197b 100644 --- a/plugin_xchat.c +++ b/plugin_xchat.c @@ -44,9 +44,10 @@ 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.0.18"; +static const char plugin_version[] = "0.0.19"; -static const char usage_setkey[] = "Usage: SETKEY [<nick or #channel>] <password>, sets the key for a channel or nick"; +static const char usage_setkey[] = "Usage: SETKEY [<nick or #channel>] [<mode>:]<password>, sets the key for a channel or nick. Modes: ECB, CBC"; +static const char usage_ciphermode[] = "Usage: CIPHERMODE [<nick or #channel>] <mode>, sets the cipher mode for a channel or nick to ECB or CBC"; static const char usage_delkey[] = "Usage: DELKEY <nick or #channel>, deletes the key for a channel or nick"; static hexchat_plugin *ph; @@ -204,6 +205,7 @@ static int handle_incoming(char *word[], char *word_eol[], void *userdata) { static int handle_setkey(char *word[], char *word_eol[], void *userdata) { const char *nick; const char *key; + bool is_cbc = false; // Check syntax if (*word[2] == '\0') { @@ -221,11 +223,61 @@ static int handle_setkey(char *word[], char *word_eol[], void *userdata) { key = word_eol[3]; } + if (!strncmp("cbc:", key, 4) || !strncmp("CBC:", key, 4)) { + key = key+4; + is_cbc = true; + } else if (!strncmp("ecb:", key, 4) || !strncmp("ECB:", key, 4)) { + 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, is_cbc)) { + hexchat_printf(ph, "Stored key for %s (%s)\n", nick, is_cbc ? "CBC mode" : "old insecure ECB mode"); + } else { + hexchat_printf(ph, "\00305Failed to store key in blow.ini\n"); + } + + return HEXCHAT_EAT_HEXCHAT; +} + +/** + * Command handler for /ciphermode + */ +static int handle_ciphermode(char *word[], char *word_eol[], void *userdata) { + const char *nick; + const char *mode; + bool is_cbc; + + // Check syntax + if (*word[2] == '\0') { + hexchat_printf(ph, "%s\n", usage_ciphermode); + return HEXCHAT_EAT_HEXCHAT; + } + + if (*word[3] == '\0') { + // /ciphermode mode + nick = hexchat_get_info(ph, "channel"); + mode = word_eol[2]; + } else { + // /ciphermode #channel mode + nick = word[2]; + mode = word_eol[3]; + } + + if (!strcmp("cbc", mode) || !strcmp("CBC", mode)) { + is_cbc = true; + } else if (!strcmp("ecb", mode) || !strcmp("ECB", mode)) { + is_cbc = false; + } else { + hexchat_printf(ph, "Invalid mode: %s. Only ECB and CBC are supported. CBC is recommended.\n"); + return HEXCHAT_EAT_HEXCHAT; + } + + // Set mode + if (keystore_set_mode(nick, is_cbc)) { + hexchat_printf(ph, "Changed mode for %s to %s\n", nick, is_cbc ? "CBC mode" : "old insecure ECB mode"); } else { - hexchat_printf(ph, "\00305Failed to store key in blow.ini\n", nick, key); + hexchat_printf(ph, "\00305Failed to set mode for nick %s\n", nick); } return HEXCHAT_EAT_HEXCHAT; @@ -282,6 +334,7 @@ int hexchat_plugin_init(hexchat_plugin *plugin_handle, /* Register commands */ hexchat_hook_command(ph, "SETKEY", HEXCHAT_PRI_NORM, handle_setkey, usage_setkey, NULL); + hexchat_hook_command(ph, "CIPHERMODE", HEXCHAT_PRI_NORM, handle_ciphermode, usage_ciphermode, NULL); hexchat_hook_command(ph, "DELKEY", HEXCHAT_PRI_NORM, handle_delkey, usage_delkey, NULL); /* Add handlers */ @@ -30,12 +30,28 @@ #include "keystore.h" // We can't use the XChat plugin API from here... +// Note that this function is used from keystore.c gchar *get_config_filename() { const gchar *homedir = g_get_home_dir(); return g_build_filename(homedir, ".config", "hexchat", "blow.ini", NULL); } +static bool prompt(const char *message, bool def) { + while (1) { + char resp[100]; + fprintf(stderr, "%s [%s] ", message, def ? "Y/n" : "y/N"); + if (!fgets(resp, sizeof(resp), stdin)) { abort(); } + if (resp[0] == '\0' || resp[0] == '\n' || resp[0] == '\r') { + return def; + } + if (resp[0] == 'Y' || resp[0] == 'y') return true; + if (resp[0] == 'N' || resp[0] == 'n') return false; + fprintf(stderr, "Please answer Y or N."); + } +} + + static int decrypt(int nick_count, char *nicks[]) { char encrypted[8192]; while (fgets(encrypted, sizeof(encrypted), stdin)) { @@ -93,30 +109,65 @@ static int setkeys(int nick_count, char *nicks[]) { char *newline = strchr(password, '\n'); if (newline) *newline = '\0'; - if (!keystore_store_key(nicks[i], password)) { - fprintf(stderr, "Failed to update key file.\n"); + fprintf(stderr, "Older plugins/clients only support the insecure ECB mode.\n"); + bool use_cbc = prompt("Enable encryption in the more secure CBC mode by default?", 1); + + if (!keystore_store_key(nicks[i], password, use_cbc)) { + fprintf(stderr, "Failed to update key file, for nick %s.\n", nicks[i]); return 1; } } return 0; } +static int setmode(int argc, char *argv[]) { + if (argc < 2) { + fprintf(stderr, "-m option requires a mode argument (CBC or ECB) and at least one nick name\n"); + return 2; + } + const char *mode = argv[0]; + bool is_cbc; + if (!strcmp(mode, "ecb") || !strcmp(mode, "ECB")) { + is_cbc = false; + } else if (!strcmp(mode, "cbc") || !strcmp(mode, "CBC")) { + is_cbc = true; + } else { + fprintf(stderr, "Invalid mode, must be CBC or ECB\n"); + return 2; + } + + int error = 0; + for (int i = 1; i < argc; i++) { + if (!keystore_set_mode(argv[i], is_cbc)) { + fprintf(stderr, "Failed to set mode for nick %s.\n", argv[i]); + error = 1; + } + } + return error; +} + int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "usage: %s [-e] nick...\n" - "usage: %s -k nick...\n" + " %s -k nick...\n" + " %s -m MODE nick...\n" "\n" "Options:\n" - " -e Encrypt (default is to decrypt\n" + " -e Encrypt (default is to decrypt)\n" " -k Set key (will prompt for password)\n" + " -m MODE Set default encryption mode for key.\n" + " Supported modes: ecb (default), cbc (recommended)\n" "\n" "This tool will read/write keys from %s\n", - argv[0], argv[0], get_config_filename()); + argv[0], argv[0], argv[0], get_config_filename()); return 2; } + if (strcmp(argv[1], "-k") == 0) { return setkeys(argc-2, &argv[2]); + } else if (strcmp(argv[1], "-m") == 0) { + return setmode(argc-2, &argv[2]); } else if (strcmp(argv[1], "-e") == 0) { return encrypt(argc-2, &argv[2]); } else { |