# -*- coding: utf-8 -*-
#
# Copyright (C) 2017-2020 Marcin Kurczewski <rr-@sakuya.pl>
# Copyright (C) 2017 Ricardo Ferreira <ricardo.sff@goatse.cx>
# Copyright (C) 2014 Charles Franklin <jakhead@gmail.com>
# Copyright (C) 2012 Markus Näsman <markus@botten.org>
# Copyright (C) 2011 David Flatz <david@upcs.at>
# Copyright (C) 2009 Bjorn Edstrom <be@bjrn.se>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

#
# NOTE: Blowfish and DH1080 implementation is licenced under a different
# license:
#
# Copyright (c) 2009, Bjorn Edstrom <be@bjrn.se>
#
# 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.
#

#
# Suggestions, Bugs, ...?
# https://github.com/freshprince/weechat-fish

#
# NOTE ABOUT DH1080:
# =================
#
# Diffie-Hellman key exchange assumes that you already have
# authenticated channels between Alice and Bob.  Which means that Alice
# has to be sure that she is really talking to Bob and not to any man in
# the middle.  But since the whole idea of FiSH is that you want to
# encrypt your communication on the IRC server whose operators you do
# not trust, there is no reliable way for Alice to tell if she really is
# talking to Bob.  It could also be some rogue IRC admin impersonating
# Bob with a fake hostname and ident or even doing a MITM attack on
# DH1080.  This means you can consider using DH1080 key exchange over
# IRC utterly broken in terms of security.
#

from __future__ import print_function

import hashlib
import re
import struct
from os import urandom

SCRIPT_NAME = "fish"
SCRIPT_AUTHOR = "David Flatz <david@upcs.at>"
SCRIPT_VERSION = "0.9.5"
SCRIPT_LICENSE = "GPL3"
SCRIPT_DESC = "FiSH for weechat"
CONFIG_FILE_NAME = SCRIPT_NAME

import_ok = True


try:
    import weechat
except ImportError:
    print("This script must be run under WeeChat.")
    print("Get WeeChat now at: https://weechat.org/")
    import_ok = False

try:
    import Crypto.Cipher.Blowfish
except:
    print("Python Cryptography Toolkit must be installed to use fish")
    import_ok = False


#
# GLOBALS
#

fish_config_file = None
fish_config_section = {}
fish_config_option = {}
fish_keys = {}
fish_cyphers = {}
fish_DH1080ctx = {}
fish_encryption_announced = {}

fish_secure_key = ""
fish_secure_cipher = None

#
# CONFIG
#


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

    option = weechat.config_new_option(
        config_file,
        section_name,
        option_name,
        "string",
        "key",
        "",
        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


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 != None:
            ### ENCRYPT Targets/Keys ###
            weechat.config_write_line(
                config_file,
                blowcrypt_pack(target, fish_secure_cipher),
                blowcrypt_pack(key, fish_secure_cipher),
            )

        else:
            weechat.config_write_line(config_file, target, key)

    return weechat.WEECHAT_RC_OK


def fish_config_init():
    global fish_config_file, fish_config_section, fish_config_option
    global fish_secure_cipher

    fish_config_file = weechat.config_new(
        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"]:
        weechat.config_free(fish_config_file)
        return

    fish_config_option["announce"] = weechat.config_new_option(
        fish_config_file,
        fish_config_section["look"],
        "announce",
        "boolean",
        "annouce if messages are being encrypted or not",
        "",
        0,
        0,
        "on",
        "on",
        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 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 messages",
        "",
        0,
        0,
        "*",
        "*",
        0,
        "",
        "",
        "",
        "",
        "",
        "",
    )

    # color
    fish_config_section["color"] = weechat.config_new_section(
        fish_config_file, "color", 0, 0, "", "", "", "", "", "", "", "", "", ""
    )
    if not fish_config_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,
        "",
        "",
        "",
        "",
        "",
        "",
    )

    # 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"]:
        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)


class MalformedError(RuntimeError):
    pass

##
## Blowfish and DH1080 Code:
##
#
# BLOWFISH
#


class Blowfish:
    def __init__(self, key=None):
        if key:
            if len(key) > 72:
                key = key[:72]
            self.blowfish = Crypto.Cipher.Blowfish.new(
                key, Crypto.Cipher.Blowfish.MODE_ECB
            )

    def decrypt(self, data):
        return self.blowfish.decrypt(data)

    def encrypt(self, data):
        return self.blowfish.encrypt(data)


# XXX: Unstable.
def blowcrypt_b64encode(s):
    """A non-standard base64-encode."""
    B64 = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    res = ""
    while s:
        left, right = struct.unpack(">LL", s[:8])
        for i in range(6):
            res += B64[right & 0x3F]
            right >>= 6
        for i in range(6):
            res += B64[left & 0x3F]
            left >>= 6
        s = s[8:]
    return res


def blowcrypt_b64decode(s):
    """A non-standard base64-decode."""
    B64 = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    res = b""
    while s:
        left, right = 0, 0
        for i, p in enumerate(s[0:6]):
            right |= B64.index(p) << (i * 6)
        for i, p in enumerate(s[6:12]):
            left |= B64.index(p) << (i * 6)
        res += bytes(
            [
                ((left & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8))
                for i in range(0, 4)
            ]
        )
        res += bytes(
            [
                ((right & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8))
                for i in range(0, 4)
            ]
        )
        s = s[12:]
    return res


def padto(msg, length):
    """Pads 'msg' with zeroes until it's length is divisible by 'length'.
    If the length of msg is already a multiple of 'length', does nothing."""
    L = len(msg)
    if L % length:
        msg += bytes(length - L % length)
    assert len(msg) % length == 0
    return msg


def blowcrypt_pack(msg, cipher):
    """."""
    return "+OK " + blowcrypt_b64encode(cipher.encrypt(padto(msg.encode('utf-8'), 8)))


def blowcrypt_unpack(msg, cipher):
    """."""
    if not (msg.startswith("+OK ") or msg.startswith("mcps ")):
        raise ValueError
    _, rest = msg.split(" ", 1)
    if len(rest) < 12:
        raise MalformedError

    if not (len(rest) % 12) == 0:
        rest = rest[: -(len(rest) % 12)]

    try:
        raw = blowcrypt_b64decode(padto(rest.encode('utf-8'), 12).decode('utf-8'))
    except TypeError:
        raise MalformedError
    if not raw:
        raise MalformedError

    try:
        plain = cipher.decrypt(raw)
    except ValueError:
        raise MalformedError

    return plain.strip(b"\x00").decode('utf-8').replace("\n", "")


#
# DH1080
#

g_dh1080 = 2
p_dh1080 = int(
    "FBE1022E23D213E8ACFA9AE8B9DFAD"
    "A3EA6B7AC7A7B7E95AB5EB2DF85892"
    "1FEADE95E6AC7BE7DE6ADBAB8A783E"
    "7AF7A7FA6A2B7BEB1E72EAE2B72F9F"
    "A2BFB2A2EFBEFAC868BADB3E828FA8"
    "BADFADA3E4CC1BE7E8AFE85E9698A7"
    "83EB68FA07A77AB6AD7BEB618ACF9C"
    "A2897EB28A6189EFA07AB99A8A7FA9"
    "AE299EFA7BA66DEAFEFBEFBF0B7D8B",
    16,
)
q_dh1080 = (p_dh1080 - 1) // 2


def dh1080_b64encode(s):
    """A non-standard base64-encode."""
    b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    d = [0] * len(s) * 2

    L = len(s) * 8
    m = 0x80
    i, j, k, t = 0, 0, 0, 0
    while i < L:
        o = ord(s[i >> 3]) if isinstance(s, str) else s[i >> 3]
        if o & m:
            t |= 1
        j += 1
        m >>= 1
        if not m:
            m = 0x80
        if not j % 6:
            d[k] = b64[t]
            t &= 0
            k += 1
        t <<= 1
        t %= 0x100
        #
        i += 1
    m = 5 - j % 6
    t <<= m
    t %= 0x100
    if m:
        d[k] = b64[t]
        k += 1
    d[k] = 0
    res = ""
    for q in d:
        if q == 0:
            break
        res += q
    return res


def dh1080_b64decode(s):
    """A non-standard base64-encode."""
    b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    buf = [0] * 256
    for i in range(64):
        buf[ord(b64[i])] = i

    L = len(s)
    if L < 2:
        raise ValueError
    for i in reversed(range(L - 1)):
        if buf[ord(s[i])] == 0:
            L -= 1
        else:
            break
    if L < 2:
        raise ValueError

    d = [0] * L
    i, k = 0, 0
    while True:
        i += 1
        if k + 1 < L:
            d[i - 1] = buf[ord(s[k])] << 2
            d[i - 1] %= 0x100
        else:
            break
        k += 1
        if k < L:
            d[i - 1] |= buf[ord(s[k])] >> 4
        else:
            break
        i += 1
        if k + 1 < L:
            d[i - 1] = buf[ord(s[k])] << 4
            d[i - 1] %= 0x100
        else:
            break
        k += 1
        if k < L:
            d[i - 1] |= buf[ord(s[k])] >> 2
        else:
            break
        i += 1
        if k + 1 < L:
            d[i - 1] = buf[ord(s[k])] << 6
            d[i - 1] %= 0x100
        else:
            break
        k += 1
        if k < L:
            d[i - 1] |= buf[ord(s[k])] % 0x100
        else:
            break
        k += 1
    return "".join(map(chr, d[0 : i - 1]))


def dh_validate_public(public, q, p):
    """See RFC 2631 section 2.1.5."""
    return 1 == pow(public, q, p)


class DH1080Ctx:
    """DH1080 context."""

    def __init__(self):
        self.public = 0
        self.private = 0
        self.secret = 0
        self.state = 0

        bits = 1080
        while True:
            self.private = bytes2int(urandom(bits // 8))
            self.public = pow(g_dh1080, self.private, p_dh1080)
            if (
                2 <= self.public <= p_dh1080 - 1
                and dh_validate_public(self.public, q_dh1080, p_dh1080) == 1
            ):
                break


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))


def dh1080_unpack(msg, ctx):
    """."""
    if not msg.startswith("DH1080_"):
        raise ValueError

    invalidmsg = (
        "Key does not validate per RFC 2631. This check is not "
        "performed by any DH1080 implementation, so we use the key "
        "anyway. See RFC 2785 for more details."
    )

    if ctx.state == 0:
        if not msg.startswith("DH1080_INIT "):
            raise MalformedError
        ctx.state = 1
        try:
            cmd, public_raw = msg.split(" ", 1)
            public = bytes2int(dh1080_b64decode(public_raw))

            if not 1 < public < p_dh1080:
                raise MalformedError

            if not dh_validate_public(public, q_dh1080, p_dh1080):
                # print(invalidmsg)
                pass

            ctx.secret = pow(public, ctx.private, p_dh1080)
        except:
            raise MalformedError

    elif ctx.state == 1:
        if not msg.startswith("DH1080_FINISH "):
            raise MalformedError
        ctx.state = 1
        try:
            cmd, public_raw = msg.split(" ", 1)
            public = bytes2int(dh1080_b64decode(public_raw))

            if not 1 < public < p_dh1080:
                raise MalformedError

            if not dh_validate_public(public, q_dh1080, p_dh1080):
                # print(invalidmsg)
                pass

            ctx.secret = pow(public, ctx.private, p_dh1080)
        except:
            raise MalformedError

    return True


def dh1080_secret(ctx):
    """."""
    if ctx.secret == 0:
        raise ValueError
    return dh1080_b64encode(sha256(int2bytes(ctx.secret)))


def bytes2int(b):
    """Variable length big endian to integer."""
    n = 0
    for p in b:
        n *= 256
        n += ord(p) if isinstance(p, str) else p
    return n


def int2bytes(n):
    """Integer to variable length big endian."""
    if n == 0:
        return "\x00"
    b = ""
    while n:
        b = chr(n % 256) + b
        n //= 256
    return b


def sha256(s):
    """sha256"""
    return hashlib.sha256(s.encode()).digest()


##
##  END Blowfish and DH1080 Code
##
#
# 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

    match = re.match(
        r"^(:(.*?)!.*? NOTICE (.*?) :)((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
    if not match or not match.group(5):
        return string

    if match.group(3) != weechat.info_get("irc_nick", server_name):
        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 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 sucessful" % target)

        fish_keys[targetl] = dh1080_secret(fish_DH1080ctx[targetl])
        if targetl in fish_cyphers:
            del fish_cyphers[targetl]
        del fish_DH1080ctx[targetl]

        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]

        return ""

    if match.group(5) in ["+OK ", "mcps "]:
        if targetl not in fish_keys:
            fish_announce_unencrypted(buffer, target)
            return string

        if targetl not in fish_cyphers:
            b = Blowfish(fish_keys[targetl])
            fish_cyphers[targetl] = b
        else:
            b = fish_cyphers[targetl]

        clean = blowcrypt_unpack(match.group(4), b)

        fish_announce_encrypted(buffer, target)

        return "%s%s" % (match.group(1), fish_msg_w_marker(clean))

    fish_announce_unencrypted(buffer, target)

    return string


def fish_modifier_in_privmsg_cb(data, modifier, server_name, string):
    global fish_keys, fish_cyphers

    match = re.match(
        r"^(:(.*?)!.*? PRIVMSG (.*?) :)(\x01ACTION )?((\+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)

        return string

    if targetl not in fish_keys:
        fish_announce_unencrypted(buffer, target)

        return string

    fish_announce_encrypted(buffer, target)

    if targetl not in fish_cyphers:
        b = Blowfish(fish_keys[targetl])
        fish_cyphers[targetl] = b
    else:
        b = fish_cyphers[targetl]
    clean = blowcrypt_unpack(match.group(5), b)

    if not match.group(4):
        return "%s%s" % (match.group(1), fish_msg_w_marker(clean))

    return "%s%s%s\x01" % (
        match.group(1),
        match.group(4),
        fish_msg_w_marker(clean),
    )


def fish_modifier_in_topic_cb(data, modifier, server_name, string):
    global fish_keys, fish_cyphers

    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:
        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)

        return string

    if targetl not in fish_cyphers:
        b = Blowfish(fish_keys[targetl])
        fish_cyphers[targetl] = b
    else:
        b = fish_cyphers[targetl]
    clean = blowcrypt_unpack(match.group(3), b)

    fish_announce_encrypted(buffer, target)

    return "%s%s" % (match.group(1), fish_msg_w_marker(clean))


def fish_modifier_in_332_cb(data, modifier, server_name, string):
    global fish_keys, fish_cyphers

    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))
    )

    if targetl not in fish_keys or not match.group(4):
        fish_announce_unencrypted(buffer, target)

        return string

    if targetl not in fish_cyphers:
        b = Blowfish(fish_keys[targetl])
        fish_cyphers[targetl] = b
    else:
        b = fish_cyphers[targetl]

    clean = blowcrypt_unpack(match.group(3), b)

    fish_announce_encrypted(buffer, target)

    return "%s%s" % (match.group(1), fish_msg_w_marker(clean))


def fish_modifier_out_privmsg_cb(data, modifier, server_name, string):
    global fish_keys, fish_cyphers

    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))
    )

    if targetl not in fish_keys:
        fish_announce_unencrypted(buffer, target)

        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)), 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

    match = re.match(r"^(TOPIC (.*?) :)(.*)$", string)
    if not match:
        return string
    if not match.group(3):
        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:
        fish_announce_unencrypted(buffer, target)

        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(match.group(3), b)

    fish_announce_encrypted(buffer, target)

    return "%s%s" % (match.group(1), cypher)


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))


def fish_unload_cb():
    fish_config_write()

    return weechat.WEECHAT_RC_OK


#
# COMMANDS
#


def fish_cmd_blowkey(data, buffer, args):
    global fish_keys, fish_cyphers, fish_DH1080ctx
    global fish_config_option, fish_secure_cipher

    if args == "" or args == "list":
        fish_list_keys(buffer)

        return weechat.WEECHAT_RC_OK

    elif args == "genkey":
        fish_secure_genkey(buffer)
        return weechat.WEECHAT_RC_OK

    argv = args.split(" ")

    if len(argv) > 2 and argv[1] == "-server":
        server_name = argv[2]
        del argv[2]
        del argv[1]
        pos = args.find(" ")
        pos = args.find(" ", pos + 1)
        args = args[pos + 1 :]
    else:
        server_name = weechat.buffer_get_string(buffer, "localvar_server")

    buffer_type = weechat.buffer_get_string(buffer, "localvar_type")
    # if no target user has been specified grab the one from the buffer if it is private
    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 len(argv) == 2
    ):
        target_user = weechat.buffer_get_string(buffer, "localvar_channel")
    elif len(argv) < 2:
        return weechat.WEECHAT_RC_ERROR
    else:
        target_user = argv[1]

    argv2eol = ""
    pos = args.find(" ")
    if pos:
        pos = args.find(" ", pos + 1)
        if pos > 0:
            argv2eol = args[pos + 1 :]
        else:
            argv2eol = args[args.find(" ") + 1 :]

    target = "%s/%s" % (server_name, target_user)
    targetl = ("%s/%s" % (server_name, target_user)).lower()

    if argv[0] == "set":
        fish_keys[targetl] = argv2eol

        if targetl in fish_cyphers:
            del fish_cyphers[targetl]

        weechat.prnt(buffer, "set key for %s to %s" % (target, argv2eol))

        return weechat.WEECHAT_RC_OK

    if argv[0] == "remove":
        if not len(argv) == 2:
            return weechat.WEECHAT_RC_ERROR

        if targetl not in fish_keys:
            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)

        return weechat.WEECHAT_RC_OK

    if argv[0] == "exchange":
        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),
        )

        return weechat.WEECHAT_RC_OK

    return weechat.WEECHAT_RC_ERROR


#
# 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 == "":
        fish_success()
        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()
            fish_success()
            return

        else:
            global SCRIPT_NAME
            fish_secure_error()
            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()
        fish_success()
        return


def fish_decrypt_keys():
    global fish_keys, fish_secure_cipher
    global fish_cyphers

    fish_keys_tmp = {}
    for target, key in fish_keys.items():
        ### DECRYPT Targets/Keys ###
        fish_keys_tmp[
            blowcrypt_unpack(target, fish_secure_cipher)
        ] = blowcrypt_unpack(key, fish_secure_cipher)

    fish_keys = fish_keys_tmp


def fish_success():
    weechat.prnt(
        "",
        "%s%sblowkey: succesfully loaded\n"
        % (weechat.prefix("join"), weechat.color("_green")),
    )


def fish_secure_error():
    """print error message if secdata not decrypted"""

    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)


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)


def fish_announce_encrypted(buffer, target):
    global fish_encryption_announced, fish_config_option

    if not weechat.config_boolean(
        fish_config_option["announce"]
    ) or fish_encryption_announced.get(target):
        return

    (server, nick) = target.split("/")

    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")

    fish_alert(buffer, "Messages to/from %s are encrypted." % target)

    fish_encryption_announced[target] = True


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):
        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"),
        ),
    )

    del fish_encryption_announced[target]


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))


def fish_list_keys(buffer):
    global fish_keys

    weechat.prnt(buffer, "\tFiSH Keys: form target(server): key")

    if len(fish_keys) == 0:
        weechat.prnt(buffer, "NO KEYS!\n")
        return

    for (target, key) in sorted(fish_keys.items()):
        (server, nick) = target.split("/")
        weechat.prnt(buffer, "\t%s(%s): %s" % (nick, server, key))


def fish_msg_w_marker(msg):
    marker = weechat.config_string(fish_config_option["mark_encrypted"])
    if weechat.config_string(fish_config_option["mark_position"]) == "end":
        return "%s%s" % (msg, marker)
    elif weechat.config_string(fish_config_option["mark_position"]) == "begin":
        return "%s%s" % (marker, msg)
    else:
        return msg


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


#
# MAIN
#

if (
    __name__ == "__main__"
    and import_ok
    and weechat.register(
        SCRIPT_NAME,
        SCRIPT_AUTHOR,
        SCRIPT_VERSION,
        SCRIPT_LICENSE,
        SCRIPT_DESC,
        "fish_unload_cb",
        "",
    )
):

    weechat.hook_command(
        "blowkey",
        "Manage FiSH keys",
        "[list] | [genkey] |set [-server <server>] [<target>] <key> "
        "| remove [-server <server>] <target> "
        "| exchange [-server <server>] [<nick>]",
        "Add, change or remove key for target or perform DH1080\n"
        "keyexchange with <nick>.\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\n"
        "\n** stores keys in plaintext by default **\n\n"
        "DH1080:                    /blowkey exchange nick\n"
        "\nPlease read the source for a note about DH1080 key exchange\n",
        "list"
        "|| genkey"
        "|| set %(irc_channel)|%(nicks)|-server %(irc_servers) %- "
        "|| remove %(irc_channel)|%(nicks)|-server %(irc_servers) %- "
        "|| exchange %(nick)|-server %(irc_servers) %-",
        "fish_cmd_blowkey",
        "",
    )

    fish_config_init()
    fish_config_read()
    fish_secure()

    weechat.hook_modifier("irc_in2_notice", "fish_modifier_in_notice_cb", "")
    weechat.hook_modifier("irc_in2_privmsg", "fish_modifier_in_privmsg_cb", "")
    weechat.hook_modifier("irc_in2_topic", "fish_modifier_in_topic_cb", "")
    weechat.hook_modifier("irc_in2_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(
        "input_text_for_buffer", "fish_modifier_input_text", ""
    )
    weechat.hook_config("fish.secure.key", "fish_secure_key_cb", "")
