Created
May 19, 2017 11:23
-
-
Save Bandie/039d389ca6928c822f1ca18f10ec5e1e to your computer and use it in GitHub Desktop.
Blowfish weechat plugin "fish.py"; python3.4 compatible
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
# | |
# Copyright (C) 2017 Marcin Kurczewski <[email protected]> | |
# Copyright (C) 2017 Ricardo Ferreira <[email protected]> | |
# Copyright (C) 2014 Charles Franklin <[email protected]> | |
# Copyright (C) 2012 Markus Näsman <[email protected]> | |
# Copyright (C) 2011 David Flatz <[email protected]> | |
# Copyright (C) 2009 Bjorn Edstrom <[email protected]> | |
# | |
# 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 <[email protected]> | |
# | |
# 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. | |
# | |
SCRIPT_NAME = "fish" | |
SCRIPT_AUTHOR = "David Flatz <[email protected]>" | |
SCRIPT_VERSION = "0.9.2" | |
SCRIPT_LICENSE = "GPL3" | |
SCRIPT_DESC = "FiSH for weechat" | |
CONFIG_FILE_NAME = SCRIPT_NAME | |
import_ok = True | |
import re | |
import struct | |
import hashlib | |
from os import urandom | |
try: | |
import weechat | |
except ImportError: | |
print("This script must be run under WeeChat.") | |
print("Get WeeChat now at: http://www.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) | |
## | |
## 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) | |
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 = '' | |
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) | |
for i in range(0,4): | |
res +=chr(((left & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8))) | |
for i in range(0,4): | |
res +=chr(((right & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8))) | |
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 += '\x00' * (length - L % length) | |
assert len(msg) % length == 0 | |
return msg | |
def blowcrypt_pack(msg, cipher): | |
""".""" | |
return '+OK ' + blowcrypt_b64encode(cipher.encrypt(padto(msg, 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, 12)) | |
except TypeError: | |
raise MalformedError | |
if not raw: | |
raise MalformedError | |
try: | |
plain = cipher.decrypt(raw) | |
except ValueError: | |
raise MalformedError | |
return plain.strip('\x00').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: | |
if ord(s[i >> 3]) & 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) | |
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).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_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("input_text_for_buffer", "fish_modifier_input_text", "") | |
weechat.hook_config("fish.secure.key", "fish_secure_key_cb", "") |
Sorry, I can't reproduce it; using Python 3.6.4
.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sorry to bother you, but with
Python 3.5.3
there are several issues occurring, e.g.:TypeError: unsupported operand type(s) for +=: 'int' and 'str'
by the usage ofbytes2int(dh1080_b64decode(public_raw))
.Any chance that you take a look on it again?