diff --git a/python/fish.py b/python/fish.py index 17506076..c572cec0 100644 --- a/python/fish.py +++ b/python/fish.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2011-2022 David Flatz -# Copyright (C) 2017-2020 Marcin Kurczewski +# Copyright (C) David Flatz # Copyright (C) 2017 Ricardo Ferreira -# Copyright (C) 2014 Charles Franklin # Copyright (C) 2012 Markus Näsman # Copyright (C) 2009 Bjorn Edstrom # @@ -22,19 +20,27 @@ # # -# NOTE: Blowfish and DH1080 implementation is licenced under a different -# license: +# Changelog, Suggestions, Bugs, ...? +# https://github.com/freshprince/weechat-fish # -# Copyright (c) 2009, Bjorn Edstrom + # -# Permission to use, copy, modify, and distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. +# HINTS: +# ===== # - +# Getting long lines cut off by the irc server? Try setting +# irc.*.split_msg_max_length to something smaller: +# /set irc.server_default.split_msg_max_length 400 +# +# You can have an indicator showing whether a key is set and messages in a +# buffer are encrypted by adding the fish item to a bar: +# /blowkey setup_bar_item +# +# If you want to keep the keys stored on disk to be encrypted you can use +# weechat secure data: +# /secure set fish.foo cbc:verysecr1tkey +# /blowkey set #foo ${sec.data.fish.foo} # -# Suggestions, Bugs, ...? -# https://github.com/freshprince/weechat-fish # # NOTE ABOUT DH1080: @@ -57,14 +63,17 @@ import hashlib import base64 import sys +import traceback from os import urandom SCRIPT_NAME = "fish" SCRIPT_AUTHOR = "David Flatz " -SCRIPT_VERSION = "0.15" +SCRIPT_VERSION = "1.0rc2" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "FiSH for weechat" CONFIG_FILE_NAME = SCRIPT_NAME +BAR_ITEM_NAME = SCRIPT_NAME +TAG_NAME = SCRIPT_NAME import_ok = True @@ -72,7 +81,7 @@ import weechat except ImportError: print("This script must be run under WeeChat.") - print("Get WeeChat now at: https://weechat.org/") + print("Get WeeChat now at: http://www.weechat.org/") import_ok = False try: @@ -90,15 +99,10 @@ # fish_config_file = None -fish_config_section = {} fish_config_option = {} -fish_keys = {} -fish_cyphers = {} +fish_config_keys = None fish_DH1080ctx = {} -fish_encryption_announced = {} - -fish_secure_key = "" -fish_secure_cipher = None +fish_bar_item = None # @@ -109,120 +113,158 @@ def fish_config_reload_cb(data, config_file): return weechat.config_reload(config_file) -def fish_config_keys_read_cb(data, config_file, section_name, option_name, - value): - global fish_keys +def fish_config_keys_create_cb(data, config_file, section, option_name, value): + option = weechat.config_search_option(config_file, section, option_name) + if option: + return weechat.config_option_set(option, value, 1) option = weechat.config_new_option( - config_file, section_name, option_name, "string", "key", "", 0, 0, - "", value, 0, "", "", "", "", "", "") + config_file, section, option_name, "string", "", "", 0, 0, "", + value, 0, "", "", "", "", "", "") if not option: return weechat.WEECHAT_CONFIG_OPTION_SET_ERROR - fish_keys[option_name] = value - - return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED + weechat.bar_item_update(BAR_ITEM_NAME) + return weechat.WEECHAT_CONFIG_OPTION_SET_OK_SAME_VALUE -def fish_config_keys_write_cb(data, config_file, section_name): - global fish_keys, fish_secure_cipher - - weechat.config_write_line(config_file, section_name, "") - for target, key in sorted(fish_keys.items()): - if fish_secure_cipher is not None: - weechat.config_write_line( - config_file, blowcrypt_pack(target.encode(), fish_secure_cipher), - blowcrypt_pack(key.encode(), fish_secure_cipher)) - else: - weechat.config_write_line(config_file, target, key) - - return weechat.WEECHAT_RC_OK +def fish_config_keys_delete_cb(data, config_file, section, option): + option_name = weechat.config_option_get_string(option, 'name') + weechat.config_option_free(option) + server, name = option_name.split('/') + buffer = weechat.info_get("irc_buffer", f"{server},{name}") + if buffer: + fish_state_set(buffer, None) + return weechat.WEECHAT_CONFIG_OPTION_UNSET_OK_REMOVED def fish_config_init(): - global fish_config_file, fish_config_section, fish_config_option - global fish_secure_cipher + global fish_config_file, fish_config_option, fish_config_keys fish_config_file = weechat.config_new( - CONFIG_FILE_NAME, "fish_config_reload_cb", "") + CONFIG_FILE_NAME, "fish_config_reload_cb", "") if not fish_config_file: return # look - fish_config_section["look"] = weechat.config_new_section( - fish_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "", - "") - if not fish_config_section["look"]: + section_look = weechat.config_new_section( + fish_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "", "") + if not section_look: weechat.config_free(fish_config_file) return fish_config_option["announce"] = weechat.config_new_option( - fish_config_file, fish_config_section["look"], "announce", - "boolean", "announce if messages are being encrypted or not", "", - 0, 0, "on", "on", 0, "", "", "", "", "", "") + fish_config_file, section_look, "announce", "boolean", + "announce if messages are being encrypted or not", "", 0, 0, + "off", "off", 0, "", "", "", "", "", "") fish_config_option["marker"] = weechat.config_new_option( - fish_config_file, fish_config_section["look"], "marker", - "string", "marker for important FiSH messages", "", 0, 0, - "O<", "O<", 0, "", "", "", "", "", "") - - fish_config_option["mark_position"] = weechat.config_new_option( - fish_config_file, fish_config_section["look"], "mark_position", - "integer", - "put marker for encrypted INCOMING messages at start or end", - "off|begin|end", 0, 2, "off", "off", 0, "", "", "", "", "", "") - - fish_config_option["mark_encrypted"] = weechat.config_new_option( - fish_config_file, fish_config_section["look"], "mark_encrypted", - "string", "marker for encrypted INCOMING messages", "", 0, 0, - "*", "*", 0, "", "", "", "", "", "") + fish_config_file, section_look, "marker", + "string", "marker for important FiSH messages", "", 0, 0, + "O<", "O<", 0, "", "", "", "", "", "") + fish_config_option["item"] = weechat.config_new_option( + fish_config_file, section_look, "item", "string", + "string used to show FiSH being used in current buffer", "", 0, 0, + "%", "%", 0, "", "", "", "", "", "") + fish_config_option["prefix"] = weechat.config_new_option( + fish_config_file, section_look, "prefix", "boolean", + "mark in prefix if message is encrypted or not", "", 0, 0, + "on", "on", 0, "", "", "", "", "", "") + fish_config_option["prefix_plaintext"] = weechat.config_new_option( + fish_config_file, section_look, "prefix.plaintext", "string", + "marker in prefix if message is plaintext", "", 0, 0, + "‼", "‼", 0, "", "", "", "", "", "") + fish_config_option["prefix_ecb"] = weechat.config_new_option( + fish_config_file, section_look, "prefix.ecb", "string", + "marker in prefix if message is encrypted in ecb mode", "", 0, 0, + "°", "°", 0, "", "", "", "", "", "") + fish_config_option["prefix_cbc"] = weechat.config_new_option( + fish_config_file, section_look, "prefix.cbc", "string", + "marker in prefix if message is encrypted in cbc mode", "", 0, 0, + "·", "·", 0, "", "", "", "", "", "") # color - fish_config_section["color"] = weechat.config_new_section( - fish_config_file, "color", 0, 0, "", "", "", "", "", "", "", "", - "", "") - if not fish_config_section["color"]: + section_color = weechat.config_new_section( + fish_config_file, "color", 0, 0, "", "", "", "", "", "", "", "", "", + "") + if not section_color: weechat.config_free(fish_config_file) return fish_config_option["alert"] = weechat.config_new_option( - fish_config_file, fish_config_section["color"], "alert", - "color", "color for important FiSH message markers", "", 0, 0, - "lightblue", "lightblue", 0, "", "", "", "", "", "") - - # secure - fish_config_section["secure"] = weechat.config_new_section( - fish_config_file, "secure", 0, 0, "", "", "", "", "", "", "", "", - "", "") - if not fish_config_section["secure"]: - weechat.config_free(fish_config_file) - return - - fish_config_option["key"] = weechat.config_new_option( - fish_config_file, fish_config_section["secure"], "key", - "string", "key for securing blowfish keys", "", 0, 0, "", "", - 0, "", "", "", "", "", "") + fish_config_file, section_color, "alert", + "color", "color for important FiSH message markers", "", 0, 0, + "lightblue", "lightblue", 0, "", "", "", "", "", "") + fish_config_option["unknown"] = weechat.config_new_option( + fish_config_file, section_color, "unknown", "color", + "color for bar item when state of encryption is unknown", "", 0, 0, + "darkgray", "darkgray", 0, "", "", "", "", "", "") + fish_config_option["plaintext"] = weechat.config_new_option( + fish_config_file, section_color, "plaintext", "color", + "color for bar item when messages are in plain text", "", 0, 0, + "*red", "*red", 0, "", "", "", "", "", "") + fish_config_option["ecb"] = weechat.config_new_option( + fish_config_file, section_color, "ecb", "color", + "color for bar item when messages are encrypted in ECB mode", "", 0, 0, + "lightblue", "lightblue", 0, "", "", "", "", "", "") + fish_config_option["cbc"] = weechat.config_new_option( + fish_config_file, section_color, "cbc", "color", + "color for bar item when messages are encrypted in CBC mode", "", 0, 0, + "green", "green", 0, "", "", "", "", "", "") # keys - fish_config_section["keys"] = weechat.config_new_section( - fish_config_file, "keys", 0, 0, "fish_config_keys_read_cb", "", - "fish_config_keys_write_cb", "", "", "", "", "", "", "") - if not fish_config_section["keys"]: + fish_config_keys = weechat.config_new_section( + fish_config_file, "keys", 1, 1, "", "", "", "", "", "", + "fish_config_keys_create_cb", "", "fish_config_keys_delete_cb", "") + if not fish_config_keys: weechat.config_free(fish_config_file) return def fish_config_read(): - global fish_config_file - return weechat.config_read(fish_config_file) def fish_config_write(): - global fish_config_file - return weechat.config_write(fish_config_file) +def fish_key_set(target: str, key: str, cbc: bool): + value = f"cbc:{key}" if cbc else key + target = target.lower() + + return fish_config_keys_create_cb( + "", fish_config_file, fish_config_keys, target, value) + + +def fish_key_get(target: str): + target = target.lower() + option = weechat.config_search_option( + fish_config_file, fish_config_keys, target) + if not option: + return None + + key = weechat.string_eval_expression( + weechat.config_string(option), {}, {}, {}) + cbc = False + if key.startswith('cbc:'): + cbc = True + key = key[4:] + + return (key, cbc) + + +def fish_key_delete(target: str): + target = target.lower() + option = weechat.config_search_option( + fish_config_file, fish_config_keys, target) + if option: + fish_config_keys_delete_cb( + "", fish_config_file, fish_config_keys, option) + return True + + return False + + ## # Blowfish and DH1080 Code: ## @@ -237,7 +279,7 @@ def __init__(self, key=None): if len(key) > 72: key = key[:72] self.blowfish = CryptoBlowfish.new( - key.encode('utf-8'), CryptoBlowfish.MODE_ECB) + key.encode('utf-8'), CryptoBlowfish.MODE_ECB) def decrypt(self, data): return self.blowfish.decrypt(data) @@ -253,10 +295,10 @@ def blowcrypt_b64encode(s): res = '' while s: left, right = struct.unpack('>LL', s[:8]) - for i in range(6): + for _ in range(6): res += B64[right & 0x3f] right >>= 6 - for i in range(6): + for _ in range(6): res += B64[left & 0x3f] left >>= 6 s = s[8:] @@ -291,18 +333,26 @@ def padto(msg, length): return msg -def blowcrypt_pack(msg, cipher): +def blowcrypt_pack(msg, key, cbc): """.""" + if cbc: + cipher = CryptoBlowfish.new( + key.encode('utf-8'), CryptoBlowfish.MODE_CBC) + return '+OK *' + base64.b64encode( + cipher.iv + cipher.encrypt(padto(msg, 8))).decode('utf-8') + + cipher = Blowfish(key) return '+OK ' + blowcrypt_b64encode(cipher.encrypt(padto(msg, 8))) -def blowcrypt_unpack(msg, cipher, key): +def blowcrypt_unpack(msg, key): """.""" if not (msg.startswith('+OK ') or msg.startswith('mcps ')): raise ValueError _, rest = msg.split(' ', 1) if rest.startswith('*'): # CBC mode + cbc = True rest = rest[1:] if len(rest) % 4: rest += '=' * (4 - len(rest) % 4) @@ -311,12 +361,14 @@ def blowcrypt_unpack(msg, cipher, key): iv = raw[:8] raw = raw[8:] - cbcCipher = CryptoBlowfish.new( - key.encode('utf-8'), CryptoBlowfish.MODE_CBC, iv) + cipher = CryptoBlowfish.new( + key.encode('utf-8'), CryptoBlowfish.MODE_CBC, iv) - plain = cbcCipher.decrypt(padto(raw, 8)) + plain = cipher.decrypt(padto(raw, 8)) else: + cbc = False + cipher = Blowfish(key) if len(rest) < 12: raise ValueError @@ -326,14 +378,14 @@ def blowcrypt_unpack(msg, cipher, key): try: raw = blowcrypt_b64decode(padto(rest, 12)) - except TypeError: - raise ValueError + except TypeError as e: + raise ValueError from e if not raw: raise ValueError plain = cipher.decrypt(raw) - return plain.strip(b'\x00').replace(b'\n', b'') + return (plain.strip(b'\x00').replace(b'\n', b''), cbc) # @@ -456,11 +508,13 @@ def dh_validate_public(public, q, p): class DH1080Ctx: """DH1080 context.""" - def __init__(self): + + def __init__(self, cbc=True): self.public = 0 self.private = 0 self.secret = 0 self.state = 0 + self.cbc = cbc bits = 1080 while True: @@ -473,13 +527,13 @@ def __init__(self): def dh1080_pack(ctx): """.""" - cmd = None if ctx.state == 0: ctx.state = 1 cmd = "DH1080_INIT " else: cmd = "DH1080_FINISH " - return cmd + dh1080_b64encode(int2bytes(ctx.public)) + return cmd + dh1080_b64encode(int2bytes(ctx.public)) + ( + " CBC" if ctx.cbc else "") def dh1080_unpack(msg, ctx): @@ -488,34 +542,39 @@ def dh1080_unpack(msg, ctx): raise ValueError if ctx.state == 0: - if not msg.startswith("DH1080_INIT "): + if (not msg.startswith("DH1080_INIT ") and + not msg.startswith("DH1080_INIT_CBC ")): raise ValueError ctx.state = 1 try: - cmd, public_raw = msg.split(' ', 1) + cmd, public_raw, *rest = msg.split(' ') public = bytes2int(dh1080_b64decode(public_raw)) if not 1 < public < p_dh1080: raise ValueError ctx.secret = pow(public, ctx.private, p_dh1080) - except Exception: - raise ValueError + ctx.cbc = "CBC" in rest or cmd == "DH1080_INIT_CBC" + + except Exception as e: + raise ValueError from e elif ctx.state == 1: if not msg.startswith("DH1080_FINISH "): raise ValueError ctx.state = 1 try: - cmd, public_raw = msg.split(' ', 1) + cmd, public_raw, *rest = msg.split(' ') public = bytes2int(dh1080_b64decode(public_raw)) if not 1 < public < p_dh1080: raise ValueError ctx.secret = pow(public, ctx.private, p_dh1080) - except Exception: - raise ValueError + ctx.cbc = "CBC" in rest + + except Exception as e: + raise ValueError from e return True @@ -529,12 +588,22 @@ def dh1080_secret(ctx): def bytes2int(b): """Variable length big endian to integer.""" - return int.from_bytes(b, byteorder='big') + n = 0 + for p in b: + n *= 256 + n += p + return n def int2bytes(n): """Integer to variable length big endian.""" - return n.to_bytes((n.bit_length() + 7) // 8, byteorder='big') + if n == 0: + return b'\x00' + b = [] + while n: + b.insert(0, n % 256) + n //= 256 + return bytes(b) def sha256(s): @@ -549,356 +618,229 @@ def sha256(s): # HOOKS # -def fish_secure_key_cb(data, option, value): - global fish_secure_key, fish_secure_cipher - - fish_secure_key = weechat.config_string( - weechat.config_get("fish.secure.key")) - - if fish_secure_key == "": - fish_secure_cipher = None - return weechat.WEECHAT_RC_OK - - if fish_secure_key[:6] == "${sec.": - decrypted = weechat.string_eval_expression(fish_secure_key, {}, {}, {}) - if decrypted: - fish_secure_cipher = Blowfish(decrypted) - return weechat.WEECHAT_RC_OK - else: - weechat.config_option_set(fish_config_option["key"], "", 0) - weechat.prnt("", "Decrypt sec.conf first\n") - return weechat.WEECHAT_RC_OK - - if fish_secure_key != "": - fish_secure_cipher = Blowfish(fish_secure_key) - - return weechat.WEECHAT_RC_OK - - def fish_modifier_in_notice_cb(data, modifier, server_name, string): - global fish_DH1080ctx, fish_keys, fish_cyphers - - if type(string) is bytes: + if isinstance(string, bytes): return string - match = re.match( - r"^((?:@[^ ]* )?:(.*?)!.*? NOTICE (.*?) :)" - r"((DH1080_INIT |DH1080_FINISH |\+OK |mcps )?.*)$", - string) - # match.group(0): message - # match.group(1): msg without payload - # match.group(2): source - # match.group(3): target - # match.group(4): msg - # match.group(5): "DH1080_INIT "|"DH1080_FINISH "|"+OK "|"mcps " - if not match or not match.group(5): - return string - - if match.group(3) != weechat.info_get("irc_nick", server_name): - return string + msg_info = weechat.info_get_hashtable('irc_message_parse', { + 'message': string, + 'server': server_name, + }) - target = "%s/%s" % (server_name, match.group(2)) - targetl = ("%s/%s" % (server_name, match.group(2))).lower() - buffer = weechat.info_get("irc_buffer", "%s,%s" % ( - server_name, match.group(2))) - - if match.group(5) == "DH1080_FINISH " and targetl in fish_DH1080ctx: - if not dh1080_unpack(match.group(4), fish_DH1080ctx[targetl]): - fish_announce_unencrypted(buffer, target) - return string - - fish_alert(buffer, "Key exchange for %s successful" % target) - - fish_keys[targetl] = dh1080_secret(fish_DH1080ctx[targetl]) - if targetl in fish_cyphers: - del fish_cyphers[targetl] - del fish_DH1080ctx[targetl] + is_direct = msg_info['channel'] == weechat.info_get( + 'irc_nick', server_name) + if is_direct: + dest = msg_info['nick'] + else: + dest = msg_info['channel'] + target = f'{server_name}/{dest}' + buffer = weechat.info_get("irc_buffer", f'{server_name},{dest}') + + text = msg_info['text'] + if (is_direct and text.startswith('DH1080_FINISH ') and + target in fish_DH1080ctx and + dh1080_unpack(text, fish_DH1080ctx[target])): + fish_alert(buffer, f'Key exchange for {target} successful') + fish_key_set(target, dh1080_secret(fish_DH1080ctx[target]), + fish_DH1080ctx[target].cbc) + del fish_DH1080ctx[target] return "" - if match.group(5) == "DH1080_INIT ": - fish_DH1080ctx[targetl] = DH1080Ctx() - - msg = ' '.join(match.group(4).split()[0:2]) - - if not dh1080_unpack(msg, fish_DH1080ctx[targetl]): - fish_announce_unencrypted(buffer, target) - return string - - reply = dh1080_pack(fish_DH1080ctx[targetl]) - - fish_alert(buffer, "Key exchange initiated by %s. Key set." % target) - - weechat.command(buffer, "/mute -all notice %s %s" % ( - match.group(2), reply)) - - fish_keys[targetl] = dh1080_secret(fish_DH1080ctx[targetl]) - if targetl in fish_cyphers: - del fish_cyphers[targetl] - del fish_DH1080ctx[targetl] + if ( + is_direct and + (text.startswith('DH1080_INIT ') or + text.startswith('DH1080_INIT_CBC ')) and + fish_DH1080ctx.__setitem__(target, DH1080Ctx()) is None and + dh1080_unpack(text, fish_DH1080ctx[target])): + reply = dh1080_pack(fish_DH1080ctx[target]) + fish_key_delete(target) + weechat.command( + buffer, f"/mute notice -server {server_name} {dest} {reply}") + fish_key_set(target, dh1080_secret( + fish_DH1080ctx[target]), fish_DH1080ctx[target].cbc) + fish_alert(buffer, f"Key exchange initiated by {target}. Key set.") + del fish_DH1080ctx[target] return "" - if match.group(5) in ["+OK ", "mcps "]: - if targetl not in fish_keys: - fish_announce_unencrypted(buffer, target) - return string - - key = fish_keys[targetl] - - try: - if targetl not in fish_cyphers: - b = Blowfish(key) - fish_cyphers[targetl] = b - else: - b = fish_cyphers[targetl] - - clean = blowcrypt_unpack(match.group(4), b, key) - - fish_announce_encrypted(buffer, target) - - return b"%s%s" % ( - match.group(1).encode(), fish_msg_w_marker(clean)) - except Exception as e: - fish_announce_unencrypted(buffer, target) - - raise e - - fish_announce_unencrypted(buffer, target) - - return string - - -def fish_modifier_in_privmsg_cb(data, modifier, server_name, string): - global fish_keys, fish_cyphers - - if type(string) is bytes: - return string - - match = re.match( - r"^((?:@[^ ]* )?:(.*?)!.*? PRIVMSG (.*?) :)(\x01ACTION )?" - r"((\+OK |mcps )?.*?)(\x01)?$", - string) - # match.group(0): message - # match.group(1): msg without payload - # match.group(2): source - # match.group(3): target - # match.group(4): action - # match.group(5): msg - # match.group(6): "+OK "|"mcps " - if not match: - return string - - if match.group(3) == weechat.info_get("irc_nick", server_name): - dest = match.group(2) - else: - dest = match.group(3) - target = "%s/%s" % (server_name, dest) - targetl = ("%s/%s" % (server_name, dest)).lower() - buffer = weechat.info_get("irc_buffer", "%s,%s" % (server_name, dest)) - - if not match.group(6): - fish_announce_unencrypted(buffer, target) - + key = fish_key_get(target) + if key is None: return string - - if targetl not in fish_keys: + if not (text.startswith('+OK ') or text.startswith('mcps ')): fish_announce_unencrypted(buffer, target) - - return string - - key = fish_keys[targetl] + return fish_tag(string) try: - if targetl not in fish_cyphers: - b = Blowfish(key) - fish_cyphers[targetl] = b - else: - b = fish_cyphers[targetl] + key, cbc = key + clean, cbc = blowcrypt_unpack(text, key) + preamble = fish_tag( + string[0:int(msg_info['pos_text'])], + 'cbc' if cbc else 'ecb') + fish_announce_encrypted(buffer, target, cbc) - clean = blowcrypt_unpack(match.group(5), b, key) + return b'%s%s' % (preamble.encode(), clean) - fish_announce_encrypted(buffer, target) - - if not match.group(4): - return b'%s%s' % ( - match.group(1).encode(), fish_msg_w_marker(clean)) - - return b"%s%s%s\x01" % ( - match.group(1).encode(), match.group(4).encode(), - fish_msg_w_marker(clean)) - - except Exception as e: + except Exception: + fish_alert('', traceback.format_exc()) fish_announce_unencrypted(buffer, target) + return fish_tag(string) - raise e - - -def fish_modifier_in_topic_cb(data, modifier, server_name, string): - global fish_keys, fish_cyphers - if type(string) is bytes: - return string - - match = re.match(r"^((?:@[^ ]* )?:.*?!.*? TOPIC (.*?) :)((\+OK |mcps )?.*)$", string) - # match.group(0): message - # match.group(1): msg without payload - # match.group(2): channel - # match.group(3): topic - # match.group(4): "+OK "|"mcps " - if not match: +def fish_modifier_in_privmsg_cb(data, modifier, server_name, string): + if isinstance(string, bytes): return string - target = "%s/%s" % (server_name, match.group(2)) - targetl = ("%s/%s" % (server_name, match.group(2))).lower() - buffer = weechat.info_get("irc_buffer", "%s,%s" % ( - server_name, match.group(2))) - - if targetl not in fish_keys or not match.group(4): - fish_announce_unencrypted(buffer, target) + msg_info = weechat.info_get_hashtable('irc_message_parse', { + 'message': string, + 'server': server_name, + }) + if msg_info['channel'] == weechat.info_get('irc_nick', server_name): + dest = msg_info['nick'] + else: + dest = msg_info['channel'] + target = f'{server_name}/{dest}' + buffer = weechat.info_get('irc_buffer', f'{server_name},{dest}') + key = fish_key_get(target) + if key is None: return string - key = fish_keys[targetl] + key, cbc = key + text = msg_info['text'] + is_action = text.startswith("\x01ACTION ") and text.endswith("\x01") + if is_action: + text = text[8:-1] + if not (text.startswith('+OK ') or text.startswith('mcps ')): + fish_announce_unencrypted(buffer, target) + return fish_tag(string) try: - if targetl not in fish_cyphers: - b = Blowfish(key) - fish_cyphers[targetl] = b - else: - b = fish_cyphers[targetl] - - clean = blowcrypt_unpack(match.group(3), b, key) - - fish_announce_encrypted(buffer, target) - - return b"%s%s" % (match.group(1).encode(), fish_msg_w_marker(clean)) - except Exception as e: + clean, cbc = blowcrypt_unpack(text, key) + if is_action: + clean = b"\x01ACTION %s\x01" % clean + preamble = fish_tag( + string[0:int(msg_info['pos_text'])], + 'cbc' if cbc else 'ecb') + fish_announce_encrypted(buffer, target, cbc) + + return b"%s%s" % ( + preamble.encode(), clean) + + except Exception: + fish_alert('', traceback.format_exc()) fish_announce_unencrypted(buffer, target) + return fish_tag(string) - raise e - - -def fish_modifier_in_332_cb(data, modifier, server_name, string): - global fish_keys, fish_cyphers - if type(string) is bytes: +def fish_modifier_in_decrypt_cb(data, modifier, server_name, string): + if isinstance(string, bytes): return string - match = re.match(r"^((?:@[^ ]* )?:.*? 332 .*? (.*?) :)((\+OK |mcps )?.*)$", string) - if not match: - return string - - target = "%s/%s" % (server_name, match.group(2)) - targetl = ("%s/%s" % (server_name, match.group(2))).lower() - buffer = weechat.info_get("irc_buffer", "%s,%s" % ( - server_name, match.group(2))) + msg_info = weechat.info_get_hashtable('irc_message_parse', { + 'message': string, + 'server': server_name, + }) - if targetl not in fish_keys or not match.group(4): - fish_announce_unencrypted(buffer, target) + target = f"{server_name}/{msg_info['channel']}" + buffer = weechat.info_get( + "irc_buffer", f"{server_name},{msg_info['channel']}") + key = fish_key_get(target) + text = msg_info['text'] + if key is None: return string - key = fish_keys[targetl] + key, cbc = key + if not text: + return fish_tag(string, 'cbc' if cbc else 'ecb') + if not (text.startswith('+OK ') or text.startswith('mcps ')): + fish_announce_unencrypted(buffer, target) + return fish_tag(string) try: - if targetl not in fish_cyphers: - b = Blowfish(key) - fish_cyphers[targetl] = b - else: - b = fish_cyphers[targetl] - - clean = blowcrypt_unpack(match.group(3), b, key) + clean, cbc = blowcrypt_unpack(text, key) + preamble = fish_tag( + string[0:int(msg_info['pos_text'])], + 'cbc' if cbc else 'ecb') + fish_announce_encrypted(buffer, target, cbc) - fish_announce_encrypted(buffer, target) + return b"%s%s" % (preamble.encode(), clean) - return b"%s%s" % (match.group(1).encode(), fish_msg_w_marker(clean)) - except Exception as e: + except Exception: + fish_alert('', traceback.format_exc()) fish_announce_unencrypted(buffer, target) + return fish_tag(string) - raise e - - -def fish_modifier_out_privmsg_cb(data, modifier, server_name, string): - global fish_keys, fish_cyphers - if type(string) is bytes: +def fish_modifier_out_encrypt_cb(data, modifier, server_name, string): + if isinstance(string, bytes): return string - match = re.match(r"^(PRIVMSG (.*?) :)(.*)$", string) - if not match: - return string - - target = "%s/%s" % (server_name, match.group(2)) - targetl = ("%s/%s" % (server_name, match.group(2))).lower() - buffer = weechat.info_get("irc_buffer", "%s,%s" % ( - server_name, match.group(2))) + msg_info = weechat.info_get_hashtable('irc_message_parse', { + 'message': string, + 'server': server_name, + }) - if targetl not in fish_keys: - fish_announce_unencrypted(buffer, target) + target = f"{server_name}/{msg_info['channel']}" + buffer = weechat.info_get( + "irc_buffer", f"{server_name},{msg_info['channel']}") + key = fish_key_get(target) + text = msg_info['text'] + if key is None: return string - if targetl not in fish_cyphers: - b = Blowfish(fish_keys[targetl]) - fish_cyphers[targetl] = b - else: - b = fish_cyphers[targetl] - cypher = blowcrypt_pack(fish_msg_wo_marker(match.group(3)).encode(), b) - - fish_announce_encrypted(buffer, target) - - return "%s%s" % (match.group(1), cypher) - - -def fish_modifier_out_topic_cb(data, modifier, server_name, string): - global fish_keys, fish_cyphers - - if type(string) is bytes: - return string + key, cbc = key + cypher = blowcrypt_pack(text.encode(), key, cbc) if text else '' + preamble = string[0:int(msg_info['pos_text'])] if text else string + fish_announce_encrypted(buffer, target, cbc) - match = re.match(r"^(TOPIC (.*?) :)(.*)$", string) - if not match: - return string - if not match.group(3): - return string + return f'{preamble}{cypher}' - target = "%s/%s" % (server_name, match.group(2)) - targetl = ("%s/%s" % (server_name, match.group(2))).lower() - buffer = weechat.info_get("irc_buffer", "%s,%s" % ( - server_name, match.group(2))) - if targetl not in fish_keys: - fish_announce_unencrypted(buffer, target) +def fish_line_cb(data: str, line): + buffer = line['buffer'] + server_name = weechat.buffer_get_string(buffer, "localvar_server") + target_user = weechat.buffer_get_string(buffer, "localvar_channel") + target = f'{server_name}/{target_user}' + key = fish_key_get(target) + if key is None: + return {} + if not weechat.config_boolean(fish_config_option['prefix']): + return {} + state = 'plaintext' + for tag in line['tags'].split(','): + if tag.startswith('irc_tag_fish='): + _, state = tag.split('=') + if tag == 'self_msg': + _, cbc = key + state = 'cbc' if cbc else 'ecb' - return string + item = weechat.config_string(fish_config_option[f'prefix_{state}']) + color = weechat.color( + weechat.config_color(fish_config_option[state])) + return {'prefix': line['prefix'] + f'{color}{item}'} - if targetl not in fish_cyphers: - b = Blowfish(fish_keys[targetl]) - fish_cyphers[targetl] = b - else: - b = fish_cyphers[targetl] - cypher = blowcrypt_pack(match.group(3).encode(), b) - fish_announce_encrypted(buffer, target) +def fish_bar_cb(data, item, window, buffer, extra_info): + server_name = weechat.buffer_get_string(buffer, "localvar_server") + target_user = weechat.buffer_get_string(buffer, "localvar_channel") + target = f"{server_name}/{target_user}" - return "%s%s" % (match.group(1), cypher) + if fish_key_get(target) is None: + return '' + state = fish_state_get(buffer, 'unknown') + item = weechat.config_string(fish_config_option['item']) + color = weechat.color(weechat.config_color(fish_config_option[state])) -def fish_modifier_input_text(data, modifier, server_name, string): - if weechat.string_is_command_char(string): - return string - buffer = weechat.current_buffer() - name = weechat.buffer_get_string(buffer, "name") - target = name.replace(".", "/") - targetl = target.lower() - if targetl not in fish_keys: - return string - return "%s" % (fish_msg_w_marker(string.encode()).decode()) + return f"{color}{item}" def fish_unload_cb(): fish_config_write() + weechat.bar_item_remove(fish_bar_item) return weechat.WEECHAT_RC_OK @@ -908,20 +850,35 @@ def fish_unload_cb(): # def fish_cmd_blowkey(data, buffer, args): - global fish_keys, fish_cyphers, fish_DH1080ctx, fish_config_option - global fish_secure_cipher + global fish_DH1080ctx - if args == "" or args == "list": + if args in ['', 'list']: fish_list_keys(buffer) return weechat.WEECHAT_RC_OK - elif args == "genkey": - fish_secure_genkey(buffer) + argv = args.split(" ") - return weechat.WEECHAT_RC_OK + if argv[0] == 'setup_bar_item': + option_name = 'weechat.bar.status.items' + option = weechat.config_get(option_name) + if option is None: + weechat.prnt(buffer, f'{option_name} not found.') + return weechat.WEECHAT_RC_ERROR + value = weechat.config_string(option) + if re.search(r'\b' + re.escape(BAR_ITEM_NAME) + r'\b', value): + weechat.prnt(buffer, 'Bar item already set up.') + return weechat.WEECHAT_RC_ERROR + if re.search(r'\bbuffer_name\b', value): + value = re.sub( + r'(buffer_name(\+[^,]*)?)', + r'\1+' + BAR_ITEM_NAME, + value) + else: + value = (value + ',' if value else '') + BAR_ITEM_NAME + weechat.command(buffer, f'/set {option_name} "{value}"') - argv = args.split(" ") + return weechat.WEECHAT_RC_OK if (len(argv) > 2 and argv[1] == "-server"): server_name = argv[2] @@ -939,7 +896,7 @@ def fish_cmd_blowkey(data, buffer, args): if argv[0] == "exchange" and len(argv) == 1 and buffer_type == "private": target_user = weechat.buffer_get_string(buffer, "localvar_channel") elif (argv[0] == "set" and - (buffer_type == "private" or buffer_type == "channel") and + buffer_type in ['private', 'channel'] and len(argv) == 2): target_user = weechat.buffer_get_string(buffer, "localvar_channel") elif len(argv) < 2: @@ -956,16 +913,18 @@ def fish_cmd_blowkey(data, buffer, args): else: argv2eol = args[args.find(" ") + 1:] - target = "%s/%s" % (server_name, target_user) - targetl = ("%s/%s" % (server_name, target_user)).lower() + target = f'{server_name}/{target_user}' if argv[0] == "set": - fish_keys[targetl] = argv2eol + cbc = False + key = argv2eol + if key.startswith('cbc:'): + cbc = True + key = argv2eol[4:] - if targetl in fish_cyphers: - del fish_cyphers[targetl] + fish_key_set(target, key, cbc) - weechat.prnt(buffer, "set key for %s to %s" % (target, argv2eol)) + weechat.prnt(buffer, f'set key for {target} to {argv2eol}') return weechat.WEECHAT_RC_OK @@ -973,15 +932,10 @@ def fish_cmd_blowkey(data, buffer, args): if not len(argv) == 2: return weechat.WEECHAT_RC_ERROR - if targetl not in fish_keys: + if not fish_key_delete(target): return weechat.WEECHAT_RC_ERROR - del fish_keys[targetl] - - if targetl in fish_cyphers: - del fish_cyphers[targetl] - - weechat.prnt(buffer, "removed key for %s" % target) + weechat.prnt(buffer, f'removed key for {target}') return weechat.WEECHAT_RC_OK @@ -989,11 +943,12 @@ def fish_cmd_blowkey(data, buffer, args): if server_name == "": return weechat.WEECHAT_RC_ERROR - weechat.prnt(buffer, "Initiating DH1080 Exchange with %s" % target) - fish_DH1080ctx[targetl] = DH1080Ctx() - msg = dh1080_pack(fish_DH1080ctx[targetl]) - weechat.command(buffer, "/mute -all notice -server %s %s %s" % ( - server_name, target_user, msg)) + weechat.prnt(buffer, f'Initiating DH1080 Exchange with {target}') + fish_DH1080ctx[target] = DH1080Ctx() + msg = dh1080_pack(fish_DH1080ctx[target]) + fish_key_delete(target) + weechat.command( + buffer, f'/mute notice -server {server_name} {target_user} {msg}') return weechat.WEECHAT_RC_OK @@ -1004,158 +959,85 @@ def fish_cmd_blowkey(data, buffer, args): # HELPERS # -def fish_secure(): - global fish_secure_key, fish_secure_cipher - - fish_secure_key = weechat.config_string(fish_config_option["key"]) - - # if blank, do nothing - if fish_secure_key == "": - return - - # if ${sec.data.fish}, check if sec.conf is decrypted - # and decrypt - elif fish_secure_key[:6] == "${sec.": - decrypted = weechat.string_eval_expression(fish_secure_key, {}, {}, {}) - - if decrypted: - fish_secure_cipher = Blowfish(decrypted) - fish_decrypt_keys() - return +def fish_tag(msg, mode=None): + tag = f'{TAG_NAME}={mode}' if mode is not None else None + if msg.startswith('@'): + msg = re.sub( + r'^@([^ ]*;|)' + + re.escape(TAG_NAME) + + r'(=[^ ;])?(;| )', r'@\1\3', + msg) + if tag is not None: + msg = re.sub(r'^@', f"@{tag};", msg) + elif tag is not None: + msg = f'@{tag} {msg}' - else: - global SCRIPT_NAME - message = ("\n%s%sblowkey:%s unable to recover key from sec.conf\n" - "%s%sblowkey:%s fish.py %sNOT LOADED\n" - "%s%sblowkey:%s decrypt secured data first\n" - "%s%sblowkey:%s then reload fish.py\n\n") % ( - weechat.prefix("error"), - weechat.color("underline"), - weechat.color("reset"), weechat.prefix("error"), - weechat.color("underline"), - weechat.color("reset"), weechat.color("*red"), - weechat.prefix("error"), - weechat.color("underline"), - weechat.color("reset"), weechat.prefix("error"), - weechat.color("underline"), - weechat.color("reset")) - - weechat.prnt("", "%s" % message) - weechat.command(weechat.current_buffer(), - "/wait 1ms /python unload %s" % SCRIPT_NAME) - return - - # if key is neither ${sec.data.fish} or "" - # encrypt/decrypt with user supplied, plain text key - if fish_secure_key != "": - fish_secure_cipher = Blowfish(fish_secure_key) - fish_decrypt_keys() - return - - -def fish_decrypt_keys(): - global fish_keys, fish_secure_cipher - global fish_cyphers - - fish_keys_tmp = {} - for target, key in fish_keys.iteritems(): - fish_keys_tmp[blowcrypt_unpack( - target, fish_secure_cipher)] = blowcrypt_unpack( - key, fish_secure_cipher) - - fish_keys = fish_keys_tmp - - -def fish_secure_genkey(buffer): - global fish_secure_cipher, fish_config_option - - newKey = blowcrypt_b64encode(urandom(32)) - - # test to see if sec.conf decrypted - weechat.command(buffer, "/secure set fish test") - decrypted = weechat.string_eval_expression("${sec.data.fish}", {}, {}, {}) - - if decrypted == "test": - weechat.config_option_set(fish_config_option["key"], - "${sec.data.fish}", 0) - fish_secure_cipher = Blowfish(newKey) - weechat.command(buffer, "/secure set fish %s" % newKey) + return msg -def fish_announce_encrypted(buffer, target): - global fish_encryption_announced, fish_config_option +def fish_announce_encrypted(buffer, target, cbc): + new_state = 'cbc' if cbc else 'ecb' - if (not weechat.config_boolean(fish_config_option['announce']) or - fish_encryption_announced.get(target)): + if fish_state_get(buffer) == new_state: return - (server, nick) = target.split("/") + server, nick = target.split('/') - if (weechat.info_get("irc_is_nick", nick) and - weechat.buffer_get_string(buffer, "localvar_type") != "private"): + if (weechat.info_get('irc_is_nick', nick) and + weechat.buffer_get_string(buffer, 'localvar_type') != 'private'): # if we get a private message and there no buffer yet, create one and # jump back to the previous buffer - weechat.command(buffer, "/mute -all query %s" % nick) - buffer = weechat.info_get("irc_buffer", "%s,%s" % (server, nick)) - weechat.command(buffer, "/input jump_previously_visited_buffer") + weechat.command(buffer, f'/mute query -server {server} {nick}') + buffer = weechat.info_get('irc_buffer', f'{server},{nick}') + weechat.command(buffer, '/input jump_previously_visited_buffer') - fish_alert(buffer, "Messages to/from %s are encrypted." % target) + if weechat.config_boolean(fish_config_option['announce']): + fish_alert( + buffer, f'Messages to/from {target} are encrypted ({new_state}).') - fish_encryption_announced[target] = True + fish_state_set(buffer, new_state) def fish_announce_unencrypted(buffer, target): - global fish_encryption_announced, fish_config_option - - if (not weechat.config_boolean(fish_config_option['announce']) or - not fish_encryption_announced.get(target)): + if fish_state_get(buffer) == 'plaintext': return - fish_alert(buffer, "Messages to/from %s are %s*not*%s encrypted." % ( - target, - weechat.color(weechat.config_color(fish_config_option["alert"])), - weechat.color("chat"))) + if weechat.config_boolean(fish_config_option['announce']): + fish_alert( + buffer, f"Messages to/from {target} are { + weechat.color( + weechat.config_color(fish_config_option['alert']))}*not*{ + weechat.color('chat')} encrypted.") - del fish_encryption_announced[target] + fish_state_set(buffer, "plaintext") def fish_alert(buffer, message): - mark = "%s%s%s\t" % ( - weechat.color(weechat.config_color(fish_config_option["alert"])), - weechat.config_string(fish_config_option["marker"]), - weechat.color("chat")) - - weechat.prnt(buffer, "%s%s" % (mark, message)) + mark = f"{ + weechat.color(weechat.config_color(fish_config_option['alert']))}{ + weechat.config_string(fish_config_option['marker'])}{ + weechat.color('chat')}" + weechat.prnt(buffer, f'{mark}\t{message}') def fish_list_keys(buffer): - global fish_keys - - weechat.prnt(buffer, "\tFiSH Keys: form target(server): key") - - for (target, key) in sorted(fish_keys.items()): - (server, nick) = target.split("/") - weechat.prnt(buffer, "\t%s(%s): %s" % (nick, server, key)) + weechat.command(buffer, f"/set {CONFIG_FILE_NAME}.keys.*") -def fish_msg_w_marker(msg): - marker = weechat.config_string(fish_config_option["mark_encrypted"]).encode() - if weechat.config_string(fish_config_option["mark_position"]) == "end": - return b"%s%s" % (msg, marker) - elif weechat.config_string(fish_config_option["mark_position"]) == "begin": - return b"%s%s" % (marker, msg) +def fish_state_set(buffer, state): + if state is None: + weechat.buffer_set(buffer, f'localvar_del_{SCRIPT_NAME}_state', '') else: - return msg + weechat.buffer_set(buffer, f'localvar_set_{SCRIPT_NAME}_state', state) + weechat.bar_item_update(BAR_ITEM_NAME) -def fish_msg_wo_marker(msg): - marker = weechat.config_string(fish_config_option["mark_encrypted"]) - if weechat.config_string(fish_config_option["mark_position"]) == "end": - return msg[0:-len(marker)] - elif weechat.config_string(fish_config_option["mark_position"]) == "begin": - return msg[len(marker):] - else: - return msg +def fish_state_get(buffer, default=None): + state = weechat.buffer_get_string(buffer, f'localvar_{SCRIPT_NAME}_state') + if not state: + state = default + + return state # @@ -1168,46 +1050,50 @@ def fish_msg_wo_marker(msg): SCRIPT_DESC, "fish_unload_cb", "")): weechat.hook_command( - "blowkey", "Manage FiSH keys", - "[list] | set [-server ] [] " - "| remove [-server ] " - "| exchange [-server ] [] " - "| genkey", - "Add, change or remove key for target or perform DH1080 key" - "exchange with .\n" - "Target can be a channel or a nick.\n" - "\n" - "Without arguments this command lists all keys.\n" - "\n" - "Examples:\n" - "Set the key for a channel: /blowkey set -server freenet #blowfish" - " key\n" - "Remove the key: /blowkey remove #blowfish\n" - "Set the key for a query: /blowkey set nick secret+key\n" - "List all keys: /blowkey\n" - "DH1080: /blowkey exchange nick\n" - "\nPlease read the source for a note about DH1080 key exchange\n", - "list || set %(irc_channel)|%(nicks)|-server %(irc_servers) %- " - "|| remove %(irc_channel)|%(nicks)|-server %(irc_servers) %- " - "|| exchange %(nick)|-server %(irc_servers) %-" - "|| genkey", - "fish_cmd_blowkey", "") + "blowkey", "Manage FiSH keys", + "[list] | set [-server ] [] " + "| remove [-server ] " + "| exchange [-server ] [] " + "| setup_bar_item", + "Add, change or remove key for target or perform DH1080 key" + "exchange with .\n" + "Target can be a channel or a nick.\n" + "\n" + "Without arguments this command lists all keys.\n" + "\n" + "Examples:\n" + "Set the key for a channel: /blowkey set -server freenet #blowfish" + " key\n" + "Remove the key: /blowkey remove #blowfish\n" + "Set the key for a query: /blowkey set nick secret+key\n" + "List all keys: /blowkey\n" + "DH1080: /blowkey exchange nick\n" + "Set up bar item: /blowkey setup_bar_item\n" + "\nPlease read the source for a note about DH1080 key exchange\n", + "list || set %(irc_channel)|%(nicks)|-server %(irc_servers) %- " + "|| remove %(irc_channel)|%(nicks)|-server %(irc_servers) %- " + "|| exchange %(nick)|-server %(irc_servers) %- " + "|| setup_bar_item", + "fish_cmd_blowkey", "") fish_config_init() fish_config_read() - fish_secure() + + fish_bar_item = weechat.bar_item_new( + '(extra)' + BAR_ITEM_NAME, 'fish_bar_cb', '') + + weechat.hook_line( + "", "", "irc_privmsg,irc_topic,irc_notice,irc_332", "fish_line_cb", "") weechat.hook_modifier("irc_in_notice", "fish_modifier_in_notice_cb", "") weechat.hook_modifier("irc_in_privmsg", "fish_modifier_in_privmsg_cb", "") - weechat.hook_modifier("irc_in_topic", "fish_modifier_in_topic_cb", "") - weechat.hook_modifier("irc_in_332", "fish_modifier_in_332_cb", "") - weechat.hook_modifier( - "irc_out_privmsg", "fish_modifier_out_privmsg_cb", "") - weechat.hook_modifier("irc_out_topic", "fish_modifier_out_topic_cb", "") + weechat.hook_modifier("irc_in_topic", "fish_modifier_in_decrypt_cb", "") + weechat.hook_modifier("irc_in_332", "fish_modifier_in_decrypt_cb", "") weechat.hook_modifier( - "input_text_for_buffer", "fish_modifier_input_text", "") - weechat.hook_config("fish.secure.key", "fish_secure_key_cb", "") + "irc_out_privmsg", "fish_modifier_out_encrypt_cb", "") + weechat.hook_modifier("irc_out_topic", "fish_modifier_out_encrypt_cb", "") + weechat.hook_modifier("irc_out_notice", "fish_modifier_out_encrypt_cb", "") elif (__name__ == "__main__" and len(sys.argv) == 3): key = sys.argv[1] msg = sys.argv[2] - print(blowcrypt_unpack(msg, Blowfish(key), key)) + print(blowcrypt_unpack(msg, key))