PageRenderTime 57ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/fish.py

https://github.com/freshprince/weechat-fish
Python | 1034 lines | 963 code | 17 blank | 54 comment | 18 complexity | 42fd0943d22caf0553386ac3d15f7ce0 MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2011-2022 David Flatz <david@upcs.at>
  4. # Copyright (C) 2017 Ricardo Ferreira <ricardo.sff@goatse.cx>
  5. # Copyright (C) 2012 Markus Näsman <markus@botten.org>
  6. # Copyright (C) 2009 Bjorn Edstrom <be@bjrn.se>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. #
  21. #
  22. # NOTE: Blowfish and DH1080 implementation is licenced under a different
  23. # license:
  24. #
  25. # Copyright (c) 2009, Bjorn Edstrom <be@bjrn.se>
  26. #
  27. # Permission to use, copy, modify, and distribute this software for any
  28. # purpose with or without fee is hereby granted, provided that the above
  29. # copyright notice and this permission notice appear in all copies.
  30. #
  31. #
  32. # Suggestions, Bugs, ...?
  33. # https://github.com/freshprince/weechat-fish
  34. #
  35. # NOTE ABOUT DH1080:
  36. # =================
  37. #
  38. # Diffie-Hellman key exchange assumes that you already have
  39. # authenticated channels between Alice and Bob. Which means that Alice
  40. # has to be sure that she is really talking to Bob and not to any man in
  41. # the middle. But since the whole idea of FiSH is that you want to
  42. # encrypt your communication on the IRC server whose operators you do
  43. # not trust, there is no reliable way for Alice to tell if she really is
  44. # talking to Bob. It could also be some rogue IRC admin impersonating
  45. # Bob with a fake hostname and ident or even doing a MITM attack on
  46. # DH1080. This means you can consider using DH1080 key exchange over
  47. # IRC utterly broken in terms of security.
  48. #
  49. import re
  50. import struct
  51. import hashlib
  52. import base64
  53. import sys
  54. from os import urandom
  55. SCRIPT_NAME = "fish"
  56. SCRIPT_AUTHOR = "David Flatz <david@upcs.at>"
  57. SCRIPT_VERSION = "0.12"
  58. SCRIPT_LICENSE = "GPL3"
  59. SCRIPT_DESC = "FiSH for weechat"
  60. CONFIG_FILE_NAME = SCRIPT_NAME
  61. import_ok = True
  62. try:
  63. import weechat
  64. except ImportError:
  65. print("This script must be run under WeeChat.")
  66. print("Get WeeChat now at: http://www.weechat.org/")
  67. import_ok = False
  68. try:
  69. import Crypto.Cipher.Blowfish
  70. except ImportError:
  71. print("Python Cryptography Toolkit must be installed to use fish")
  72. import_ok = False
  73. #
  74. # GLOBALS
  75. #
  76. fish_config_file = None
  77. fish_config_section = {}
  78. fish_config_option = {}
  79. fish_keys = {}
  80. fish_cyphers = {}
  81. fish_DH1080ctx = {}
  82. fish_encryption_announced = {}
  83. #
  84. # CONFIG
  85. #
  86. def fish_config_reload_cb(data, config_file):
  87. return weechat.config_reload(config_file)
  88. def fish_config_keys_read_cb(data, config_file, section_name, option_name,
  89. value):
  90. global fish_keys
  91. option = weechat.config_new_option(
  92. config_file, section_name, option_name, "string", "key", "", 0, 0,
  93. "", value, 0, "", "", "", "", "", "")
  94. if not option:
  95. return weechat.WEECHAT_CONFIG_OPTION_SET_ERROR
  96. fish_keys[option_name] = value
  97. return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED
  98. def fish_config_keys_write_cb(data, config_file, section_name):
  99. global fish_keys
  100. weechat.config_write_line(config_file, section_name, "")
  101. for target, key in sorted(fish_keys.items()):
  102. weechat.config_write_line(config_file, target, key)
  103. return weechat.WEECHAT_RC_OK
  104. def fish_config_init():
  105. global fish_config_file, fish_config_section, fish_config_option
  106. fish_config_file = weechat.config_new(
  107. CONFIG_FILE_NAME, "fish_config_reload_cb", "")
  108. if not fish_config_file:
  109. return
  110. # look
  111. fish_config_section["look"] = weechat.config_new_section(
  112. fish_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "",
  113. "")
  114. if not fish_config_section["look"]:
  115. weechat.config_free(fish_config_file)
  116. return
  117. fish_config_option["announce"] = weechat.config_new_option(
  118. fish_config_file, fish_config_section["look"], "announce",
  119. "boolean", "announce if messages are being encrypted or not", "",
  120. 0, 0, "on", "on", 0, "", "", "", "", "", "")
  121. fish_config_option["marker"] = weechat.config_new_option(
  122. fish_config_file, fish_config_section["look"], "marker",
  123. "string", "marker for important FiSH messages", "", 0, 0,
  124. "O<", "O<", 0, "", "", "", "", "", "")
  125. # color
  126. fish_config_section["color"] = weechat.config_new_section(
  127. fish_config_file, "color", 0, 0, "", "", "", "", "", "", "", "",
  128. "", "")
  129. if not fish_config_section["color"]:
  130. weechat.config_free(fish_config_file)
  131. return
  132. fish_config_option["alert"] = weechat.config_new_option(
  133. fish_config_file, fish_config_section["color"], "alert",
  134. "color", "color for important FiSH message markers", "", 0, 0,
  135. "lightblue", "lightblue", 0, "", "", "", "", "", "")
  136. # keys
  137. fish_config_section["keys"] = weechat.config_new_section(
  138. fish_config_file, "keys", 0, 0, "fish_config_keys_read_cb", "",
  139. "fish_config_keys_write_cb", "", "", "", "", "", "", "")
  140. if not fish_config_section["keys"]:
  141. weechat.config_free(fish_config_file)
  142. return
  143. def fish_config_read():
  144. global fish_config_file
  145. return weechat.config_read(fish_config_file)
  146. def fish_config_write():
  147. global fish_config_file
  148. return weechat.config_write(fish_config_file)
  149. ##
  150. # Blowfish and DH1080 Code:
  151. ##
  152. #
  153. # BLOWFISH
  154. #
  155. class Blowfish:
  156. def __init__(self, key=None):
  157. if key:
  158. if len(key) > 72:
  159. key = key[:72]
  160. self.blowfish = Crypto.Cipher.Blowfish.new(
  161. key.encode('utf-8'), Crypto.Cipher.Blowfish.MODE_ECB)
  162. def decrypt(self, data):
  163. return self.blowfish.decrypt(data)
  164. def encrypt(self, data):
  165. return self.blowfish.encrypt(data)
  166. # XXX: Unstable.
  167. def blowcrypt_b64encode(s):
  168. """A non-standard base64-encode."""
  169. B64 = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  170. res = ''
  171. while s:
  172. left, right = struct.unpack('>LL', s[:8])
  173. for i in range(6):
  174. res += B64[right & 0x3f]
  175. right >>= 6
  176. for i in range(6):
  177. res += B64[left & 0x3f]
  178. left >>= 6
  179. s = s[8:]
  180. return res
  181. def blowcrypt_b64decode(s):
  182. """A non-standard base64-decode."""
  183. B64 = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  184. res = []
  185. while s:
  186. left, right = 0, 0
  187. for i, p in enumerate(s[0:6]):
  188. right |= B64.index(p) << (i * 6)
  189. for i, p in enumerate(s[6:12]):
  190. left |= B64.index(p) << (i * 6)
  191. for i in range(0, 4):
  192. res.append((left & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8))
  193. for i in range(0, 4):
  194. res.append((right & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8))
  195. s = s[12:]
  196. return bytes(res)
  197. def padto(msg, length):
  198. """Pads 'msg' with zeroes until it's length is divisible by 'length'.
  199. If the length of msg is already a multiple of 'length', does nothing."""
  200. L = len(msg)
  201. if L % length:
  202. msg += b'\x00' * (length - L % length)
  203. assert len(msg) % length == 0
  204. return msg
  205. def blowcrypt_pack(msg, cipher):
  206. """."""
  207. return '+OK ' + blowcrypt_b64encode(cipher.encrypt(padto(msg, 8)))
  208. def blowcrypt_unpack(msg, cipher, key):
  209. """."""
  210. if not (msg.startswith('+OK ') or msg.startswith('mcps ')):
  211. raise ValueError
  212. _, rest = msg.split(' ', 1)
  213. if rest.startswith('*'): # CBC mode
  214. rest = rest[1:]
  215. if len(rest) % 4:
  216. rest += '=' * (4 - len(rest) % 4)
  217. raw = base64.b64decode(rest)
  218. iv = raw[:8]
  219. raw = raw[8:]
  220. cbcCipher = Crypto.Cipher.Blowfish.new(
  221. key.encode('utf-8'), Crypto.Cipher.Blowfish.MODE_CBC, iv)
  222. plain = cbcCipher.decrypt(padto(raw, 8))
  223. else:
  224. if len(rest) < 12:
  225. raise ValueError
  226. if not (len(rest) % 12) == 0:
  227. rest = rest[:-(len(rest) % 12)]
  228. try:
  229. raw = blowcrypt_b64decode(padto(rest, 12))
  230. except TypeError:
  231. raise ValueError
  232. if not raw:
  233. raise ValueError
  234. plain = cipher.decrypt(raw)
  235. return plain.strip(b'\x00').replace(b'\n', b'')
  236. #
  237. # DH1080
  238. #
  239. g_dh1080 = 2
  240. p_dh1080 = int('FBE1022E23D213E8ACFA9AE8B9DFAD'
  241. 'A3EA6B7AC7A7B7E95AB5EB2DF85892'
  242. '1FEADE95E6AC7BE7DE6ADBAB8A783E'
  243. '7AF7A7FA6A2B7BEB1E72EAE2B72F9F'
  244. 'A2BFB2A2EFBEFAC868BADB3E828FA8'
  245. 'BADFADA3E4CC1BE7E8AFE85E9698A7'
  246. '83EB68FA07A77AB6AD7BEB618ACF9C'
  247. 'A2897EB28A6189EFA07AB99A8A7FA9'
  248. 'AE299EFA7BA66DEAFEFBEFBF0B7D8B', 16)
  249. q_dh1080 = (p_dh1080 - 1) // 2
  250. def dh1080_b64encode(s):
  251. """A non-standard base64-encode."""
  252. b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
  253. d = [0] * len(s) * 2
  254. L = len(s) * 8
  255. m = 0x80
  256. i, j, k, t = 0, 0, 0, 0
  257. while i < L:
  258. if s[i >> 3] & m:
  259. t |= 1
  260. j += 1
  261. m >>= 1
  262. if not m:
  263. m = 0x80
  264. if not j % 6:
  265. d[k] = b64[t]
  266. t &= 0
  267. k += 1
  268. t <<= 1
  269. t %= 0x100
  270. #
  271. i += 1
  272. m = 5 - j % 6
  273. t <<= m
  274. t %= 0x100
  275. if m:
  276. d[k] = b64[t]
  277. k += 1
  278. d[k] = 0
  279. res = ''
  280. for q in d:
  281. if q == 0:
  282. break
  283. res += q
  284. return res
  285. def dh1080_b64decode(s):
  286. """A non-standard base64-encode."""
  287. b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
  288. buf = [0] * 256
  289. for i in range(64):
  290. buf[ord(b64[i])] = i
  291. L = len(s)
  292. if L < 2:
  293. raise ValueError
  294. for i in reversed(list(range(L - 1))):
  295. if buf[ord(s[i])] == 0:
  296. L -= 1
  297. else:
  298. break
  299. if L < 2:
  300. raise ValueError
  301. d = [0] * L
  302. i, k = 0, 0
  303. while True:
  304. i += 1
  305. if k + 1 < L:
  306. d[i - 1] = buf[ord(s[k])] << 2
  307. d[i - 1] %= 0x100
  308. else:
  309. break
  310. k += 1
  311. if k < L:
  312. d[i - 1] |= buf[ord(s[k])] >> 4
  313. else:
  314. break
  315. i += 1
  316. if k + 1 < L:
  317. d[i - 1] = buf[ord(s[k])] << 4
  318. d[i - 1] %= 0x100
  319. else:
  320. break
  321. k += 1
  322. if k < L:
  323. d[i - 1] |= buf[ord(s[k])] >> 2
  324. else:
  325. break
  326. i += 1
  327. if k + 1 < L:
  328. d[i - 1] = buf[ord(s[k])] << 6
  329. d[i - 1] %= 0x100
  330. else:
  331. break
  332. k += 1
  333. if k < L:
  334. d[i - 1] |= buf[ord(s[k])] % 0x100
  335. else:
  336. break
  337. k += 1
  338. return bytes(d[0:i - 1])
  339. def dh_validate_public(public, q, p):
  340. """See RFC 2631 section 2.1.5."""
  341. return 1 == pow(public, q, p)
  342. class DH1080Ctx:
  343. """DH1080 context."""
  344. def __init__(self):
  345. self.public = 0
  346. self.private = 0
  347. self.secret = 0
  348. self.state = 0
  349. bits = 1080
  350. while True:
  351. self.private = bytes2int(urandom(bits // 8))
  352. self.public = pow(g_dh1080, self.private, p_dh1080)
  353. if 2 <= self.public <= p_dh1080 - 1 and \
  354. dh_validate_public(self.public, q_dh1080, p_dh1080) == 1:
  355. break
  356. def dh1080_pack(ctx):
  357. """."""
  358. cmd = None
  359. if ctx.state == 0:
  360. ctx.state = 1
  361. cmd = "DH1080_INIT "
  362. else:
  363. cmd = "DH1080_FINISH "
  364. return cmd + dh1080_b64encode(int2bytes(ctx.public))
  365. def dh1080_unpack(msg, ctx):
  366. """."""
  367. if not msg.startswith("DH1080_"):
  368. raise ValueError
  369. if ctx.state == 0:
  370. if not msg.startswith("DH1080_INIT "):
  371. raise ValueError
  372. ctx.state = 1
  373. try:
  374. cmd, public_raw = msg.split(' ', 1)
  375. public = bytes2int(dh1080_b64decode(public_raw))
  376. if not 1 < public < p_dh1080:
  377. raise ValueError
  378. ctx.secret = pow(public, ctx.private, p_dh1080)
  379. except Exception:
  380. raise ValueError
  381. elif ctx.state == 1:
  382. if not msg.startswith("DH1080_FINISH "):
  383. raise ValueError
  384. ctx.state = 1
  385. try:
  386. cmd, public_raw = msg.split(' ', 1)
  387. public = bytes2int(dh1080_b64decode(public_raw))
  388. if not 1 < public < p_dh1080:
  389. raise ValueError
  390. ctx.secret = pow(public, ctx.private, p_dh1080)
  391. except Exception:
  392. raise ValueError
  393. return True
  394. def dh1080_secret(ctx):
  395. """."""
  396. if ctx.secret == 0:
  397. raise ValueError
  398. return dh1080_b64encode(sha256(int2bytes(ctx.secret)))
  399. def bytes2int(b):
  400. """Variable length big endian to integer."""
  401. n = 0
  402. for p in b:
  403. n *= 256
  404. n += p
  405. return n
  406. def int2bytes(n):
  407. """Integer to variable length big endian."""
  408. if n == 0:
  409. return b'\x00'
  410. b = []
  411. while n:
  412. b.insert(0, n % 256)
  413. n //= 256
  414. return bytes(b)
  415. def sha256(s):
  416. """sha256"""
  417. return hashlib.sha256(s).digest()
  418. ##
  419. # END Blowfish and DH1080 Code
  420. ##
  421. #
  422. # HOOKS
  423. #
  424. def fish_modifier_in_notice_cb(data, modifier, server_name, string):
  425. global fish_DH1080ctx, fish_keys, fish_cyphers
  426. if type(string) is bytes:
  427. return string
  428. match = re.match(
  429. r"^(:(.*?)!.*? NOTICE (.*?) :)"
  430. r"((DH1080_INIT |DH1080_FINISH |\+OK |mcps )?.*)$",
  431. string)
  432. # match.group(0): message
  433. # match.group(1): msg without payload
  434. # match.group(2): source
  435. # match.group(3): target
  436. # match.group(4): msg
  437. # match.group(5): "DH1080_INIT "|"DH1080_FINISH "|"+OK "|"mcps "
  438. if not match or not match.group(5):
  439. return string
  440. if match.group(3) != weechat.info_get("irc_nick", server_name):
  441. return string
  442. target = "%s/%s" % (server_name, match.group(2))
  443. targetl = ("%s/%s" % (server_name, match.group(2))).lower()
  444. buffer = weechat.info_get("irc_buffer", "%s,%s" % (
  445. server_name, match.group(2)))
  446. if match.group(5) == "DH1080_FINISH " and targetl in fish_DH1080ctx:
  447. if not dh1080_unpack(match.group(4), fish_DH1080ctx[targetl]):
  448. fish_announce_unencrypted(buffer, target)
  449. return string
  450. fish_alert(buffer, "Key exchange for %s successful" % target)
  451. fish_keys[targetl] = dh1080_secret(fish_DH1080ctx[targetl])
  452. if targetl in fish_cyphers:
  453. del fish_cyphers[targetl]
  454. del fish_DH1080ctx[targetl]
  455. return ""
  456. if match.group(5) == "DH1080_INIT ":
  457. fish_DH1080ctx[targetl] = DH1080Ctx()
  458. msg = ' '.join(match.group(4).split()[0:2])
  459. if not dh1080_unpack(msg, fish_DH1080ctx[targetl]):
  460. fish_announce_unencrypted(buffer, target)
  461. return string
  462. reply = dh1080_pack(fish_DH1080ctx[targetl])
  463. fish_alert(buffer, "Key exchange initiated by %s. Key set." % target)
  464. weechat.command(buffer, "/mute -all notice %s %s" % (
  465. match.group(2), reply))
  466. fish_keys[targetl] = dh1080_secret(fish_DH1080ctx[targetl])
  467. if targetl in fish_cyphers:
  468. del fish_cyphers[targetl]
  469. del fish_DH1080ctx[targetl]
  470. return ""
  471. if match.group(5) in ["+OK ", "mcps "]:
  472. if targetl not in fish_keys:
  473. fish_announce_unencrypted(buffer, target)
  474. return string
  475. key = fish_keys[targetl]
  476. try:
  477. if targetl not in fish_cyphers:
  478. b = Blowfish(key)
  479. fish_cyphers[targetl] = b
  480. else:
  481. b = fish_cyphers[targetl]
  482. clean = blowcrypt_unpack(match.group(4), b, key)
  483. fish_announce_encrypted(buffer, target)
  484. return b"%s%s" % (match.group(1).encode(), clean)
  485. except Exception as e:
  486. fish_announce_unencrypted(buffer, target)
  487. raise e
  488. fish_announce_unencrypted(buffer, target)
  489. return string
  490. def fish_modifier_in_privmsg_cb(data, modifier, server_name, string):
  491. global fish_keys, fish_cyphers
  492. if type(string) is bytes:
  493. return string
  494. match = re.match(
  495. r"^(:(.*?)!.*? PRIVMSG (.*?) :)(\x01ACTION )?"
  496. r"((\+OK |mcps )?.*?)(\x01)?$",
  497. string)
  498. # match.group(0): message
  499. # match.group(1): msg without payload
  500. # match.group(2): source
  501. # match.group(3): target
  502. # match.group(4): action
  503. # match.group(5): msg
  504. # match.group(6): "+OK "|"mcps "
  505. if not match:
  506. return string
  507. if match.group(3) == weechat.info_get("irc_nick", server_name):
  508. dest = match.group(2)
  509. else:
  510. dest = match.group(3)
  511. target = "%s/%s" % (server_name, dest)
  512. targetl = ("%s/%s" % (server_name, dest)).lower()
  513. buffer = weechat.info_get("irc_buffer", "%s,%s" % (server_name, dest))
  514. if not match.group(6):
  515. fish_announce_unencrypted(buffer, target)
  516. return string
  517. if targetl not in fish_keys:
  518. fish_announce_unencrypted(buffer, target)
  519. return string
  520. key = fish_keys[targetl]
  521. try:
  522. if targetl not in fish_cyphers:
  523. b = Blowfish(key)
  524. fish_cyphers[targetl] = b
  525. else:
  526. b = fish_cyphers[targetl]
  527. clean = blowcrypt_unpack(match.group(5), b, key)
  528. fish_announce_encrypted(buffer, target)
  529. if not match.group(4):
  530. return b'%s%s' % (match.group(1).encode(), clean)
  531. return b"%s%s%s\x01" % (
  532. match.group(1).encode(), match.group(4).encode(), clean)
  533. except Exception as e:
  534. fish_announce_unencrypted(buffer, target)
  535. raise e
  536. def fish_modifier_in_topic_cb(data, modifier, server_name, string):
  537. global fish_keys, fish_cyphers
  538. if type(string) is bytes:
  539. return string
  540. match = re.match(r"^(:.*?!.*? TOPIC (.*?) :)((\+OK |mcps )?.*)$", string)
  541. # match.group(0): message
  542. # match.group(1): msg without payload
  543. # match.group(2): channel
  544. # match.group(3): topic
  545. # match.group(4): "+OK "|"mcps "
  546. if not match:
  547. return string
  548. target = "%s/%s" % (server_name, match.group(2))
  549. targetl = ("%s/%s" % (server_name, match.group(2))).lower()
  550. buffer = weechat.info_get("irc_buffer", "%s,%s" % (
  551. server_name, match.group(2)))
  552. if targetl not in fish_keys or not match.group(4):
  553. fish_announce_unencrypted(buffer, target)
  554. return string
  555. key = fish_keys[targetl]
  556. try:
  557. if targetl not in fish_cyphers:
  558. b = Blowfish(key)
  559. fish_cyphers[targetl] = b
  560. else:
  561. b = fish_cyphers[targetl]
  562. clean = blowcrypt_unpack(match.group(3), b, key)
  563. fish_announce_encrypted(buffer, target)
  564. return b"%s%s" % (match.group(1).encode(), clean)
  565. except Exception as e:
  566. fish_announce_unencrypted(buffer, target)
  567. raise e
  568. def fish_modifier_in_332_cb(data, modifier, server_name, string):
  569. global fish_keys, fish_cyphers
  570. if type(string) is bytes:
  571. return string
  572. match = re.match(r"^(:.*? 332 .*? (.*?) :)((\+OK |mcps )?.*)$", string)
  573. if not match:
  574. return string
  575. target = "%s/%s" % (server_name, match.group(2))
  576. targetl = ("%s/%s" % (server_name, match.group(2))).lower()
  577. buffer = weechat.info_get("irc_buffer", "%s,%s" % (
  578. server_name, match.group(2)))
  579. if targetl not in fish_keys or not match.group(4):
  580. fish_announce_unencrypted(buffer, target)
  581. return string
  582. key = fish_keys[targetl]
  583. try:
  584. if targetl not in fish_cyphers:
  585. b = Blowfish(key)
  586. fish_cyphers[targetl] = b
  587. else:
  588. b = fish_cyphers[targetl]
  589. clean = blowcrypt_unpack(match.group(3), b, key)
  590. fish_announce_encrypted(buffer, target)
  591. return b"%s%s" % (match.group(1).encode(), clean)
  592. except Exception as e:
  593. fish_announce_unencrypted(buffer, target)
  594. raise e
  595. def fish_modifier_out_privmsg_cb(data, modifier, server_name, string):
  596. global fish_keys, fish_cyphers
  597. if type(string) is bytes:
  598. return string
  599. match = re.match(r"^(PRIVMSG (.*?) :)(.*)$", string)
  600. if not match:
  601. return string
  602. target = "%s/%s" % (server_name, match.group(2))
  603. targetl = ("%s/%s" % (server_name, match.group(2))).lower()
  604. buffer = weechat.info_get("irc_buffer", "%s,%s" % (
  605. server_name, match.group(2)))
  606. if targetl not in fish_keys:
  607. fish_announce_unencrypted(buffer, target)
  608. return string
  609. if targetl not in fish_cyphers:
  610. b = Blowfish(fish_keys[targetl])
  611. fish_cyphers[targetl] = b
  612. else:
  613. b = fish_cyphers[targetl]
  614. cypher = blowcrypt_pack(match.group(3).encode(), b)
  615. fish_announce_encrypted(buffer, target)
  616. return "%s%s" % (match.group(1), cypher)
  617. def fish_modifier_out_topic_cb(data, modifier, server_name, string):
  618. global fish_keys, fish_cyphers
  619. if type(string) is bytes:
  620. return string
  621. match = re.match(r"^(TOPIC (.*?) :)(.*)$", string)
  622. if not match:
  623. return string
  624. if not match.group(3):
  625. return string
  626. target = "%s/%s" % (server_name, match.group(2))
  627. targetl = ("%s/%s" % (server_name, match.group(2))).lower()
  628. buffer = weechat.info_get("irc_buffer", "%s,%s" % (
  629. server_name, match.group(2)))
  630. if targetl not in fish_keys:
  631. fish_announce_unencrypted(buffer, target)
  632. return string
  633. if targetl not in fish_cyphers:
  634. b = Blowfish(fish_keys[targetl])
  635. fish_cyphers[targetl] = b
  636. else:
  637. b = fish_cyphers[targetl]
  638. cypher = blowcrypt_pack(match.group(3).encode(), b)
  639. fish_announce_encrypted(buffer, target)
  640. return "%s%s" % (match.group(1), cypher)
  641. def fish_unload_cb():
  642. fish_config_write()
  643. return weechat.WEECHAT_RC_OK
  644. #
  645. # COMMANDS
  646. #
  647. def fish_cmd_blowkey(data, buffer, args):
  648. global fish_keys, fish_cyphers, fish_DH1080ctx
  649. if args == "" or args == "list":
  650. fish_list_keys(buffer)
  651. return weechat.WEECHAT_RC_OK
  652. argv = args.split(" ")
  653. if (len(argv) > 2 and argv[1] == "-server"):
  654. server_name = argv[2]
  655. del argv[2]
  656. del argv[1]
  657. pos = args.find(" ")
  658. pos = args.find(" ", pos + 1)
  659. args = args[pos+1:]
  660. else:
  661. server_name = weechat.buffer_get_string(buffer, "localvar_server")
  662. buffer_type = weechat.buffer_get_string(buffer, "localvar_type")
  663. # if no target user has been specified grab the one from the buffer if it
  664. # is private
  665. if argv[0] == "exchange" and len(argv) == 1 and buffer_type == "private":
  666. target_user = weechat.buffer_get_string(buffer, "localvar_channel")
  667. elif (argv[0] == "set" and
  668. (buffer_type == "private" or buffer_type == "channel") and
  669. len(argv) == 2):
  670. target_user = weechat.buffer_get_string(buffer, "localvar_channel")
  671. elif len(argv) < 2:
  672. return weechat.WEECHAT_RC_ERROR
  673. else:
  674. target_user = argv[1]
  675. argv2eol = ""
  676. pos = args.find(" ")
  677. if pos:
  678. pos = args.find(" ", pos + 1)
  679. if pos > 0:
  680. argv2eol = args[pos + 1:]
  681. else:
  682. argv2eol = args[args.find(" ") + 1:]
  683. target = "%s/%s" % (server_name, target_user)
  684. targetl = ("%s/%s" % (server_name, target_user)).lower()
  685. if argv[0] == "set":
  686. fish_keys[targetl] = argv2eol
  687. if targetl in fish_cyphers:
  688. del fish_cyphers[targetl]
  689. weechat.prnt(buffer, "set key for %s to %s" % (target, argv2eol))
  690. return weechat.WEECHAT_RC_OK
  691. if argv[0] == "remove":
  692. if not len(argv) == 2:
  693. return weechat.WEECHAT_RC_ERROR
  694. if targetl not in fish_keys:
  695. return weechat.WEECHAT_RC_ERROR
  696. del fish_keys[targetl]
  697. if targetl in fish_cyphers:
  698. del fish_cyphers[targetl]
  699. weechat.prnt(buffer, "removed key for %s" % target)
  700. return weechat.WEECHAT_RC_OK
  701. if argv[0] == "exchange":
  702. if server_name == "":
  703. return weechat.WEECHAT_RC_ERROR
  704. weechat.prnt(buffer, "Initiating DH1080 Exchange with %s" % target)
  705. fish_DH1080ctx[targetl] = DH1080Ctx()
  706. msg = dh1080_pack(fish_DH1080ctx[targetl])
  707. weechat.command(buffer, "/mute -all notice -server %s %s %s" % (
  708. server_name, target_user, msg))
  709. return weechat.WEECHAT_RC_OK
  710. return weechat.WEECHAT_RC_ERROR
  711. #
  712. # HELPERS
  713. #
  714. def fish_announce_encrypted(buffer, target):
  715. global fish_encryption_announced, fish_config_option
  716. if (not weechat.config_boolean(fish_config_option['announce']) or
  717. fish_encryption_announced.get(target)):
  718. return
  719. (server, nick) = target.split("/")
  720. if (weechat.info_get("irc_is_nick", nick) and
  721. weechat.buffer_get_string(buffer, "localvar_type") != "private"):
  722. # if we get a private message and there no buffer yet, create one and
  723. # jump back to the previous buffer
  724. weechat.command(buffer, "/mute -all query %s" % nick)
  725. buffer = weechat.info_get("irc_buffer", "%s,%s" % (server, nick))
  726. weechat.command(buffer, "/input jump_previously_visited_buffer")
  727. fish_alert(buffer, "Messages to/from %s are encrypted." % target)
  728. fish_encryption_announced[target] = True
  729. def fish_announce_unencrypted(buffer, target):
  730. global fish_encryption_announced, fish_config_option
  731. if (not weechat.config_boolean(fish_config_option['announce']) or
  732. not fish_encryption_announced.get(target)):
  733. return
  734. fish_alert(buffer, "Messages to/from %s are %s*not*%s encrypted." % (
  735. target,
  736. weechat.color(weechat.config_color(fish_config_option["alert"])),
  737. weechat.color("chat")))
  738. del fish_encryption_announced[target]
  739. def fish_alert(buffer, message):
  740. mark = "%s%s%s\t" % (
  741. weechat.color(weechat.config_color(fish_config_option["alert"])),
  742. weechat.config_string(fish_config_option["marker"]),
  743. weechat.color("chat"))
  744. weechat.prnt(buffer, "%s%s" % (mark, message))
  745. def fish_list_keys(buffer):
  746. global fish_keys
  747. weechat.prnt(buffer, "\tFiSH Keys: form target(server): key")
  748. for (target, key) in sorted(fish_keys.items()):
  749. (server, nick) = target.split("/")
  750. weechat.prnt(buffer, "\t%s(%s): %s" % (nick, server, key))
  751. #
  752. # MAIN
  753. #
  754. if (__name__ == "__main__" and import_ok and
  755. weechat.register(
  756. SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,
  757. SCRIPT_DESC, "fish_unload_cb", "")):
  758. weechat.hook_command(
  759. "blowkey", "Manage FiSH keys",
  760. "[list] | set [-server <server>] [<target>] <key> "
  761. "| remove [-server <server>] <target> "
  762. "| exchange [-server <server>] [<nick>]",
  763. "Add, change or remove key for target or perform DH1080 key"
  764. "exchange with <nick>.\n"
  765. "Target can be a channel or a nick.\n"
  766. "\n"
  767. "Without arguments this command lists all keys.\n"
  768. "\n"
  769. "Examples:\n"
  770. "Set the key for a channel: /blowkey set -server freenet #blowfish"
  771. " key\n"
  772. "Remove the key: /blowkey remove #blowfish\n"
  773. "Set the key for a query: /blowkey set nick secret+key\n"
  774. "List all keys: /blowkey\n"
  775. "DH1080: /blowkey exchange nick\n"
  776. "\nPlease read the source for a note about DH1080 key exchange\n",
  777. "list || set %(irc_channel)|%(nicks)|-server %(irc_servers) %- "
  778. "|| remove %(irc_channel)|%(nicks)|-server %(irc_servers) %- "
  779. "|| exchange %(nick)|-server %(irc_servers) %-",
  780. "fish_cmd_blowkey", "")
  781. fish_config_init()
  782. fish_config_read()
  783. weechat.hook_modifier("irc_in_notice", "fish_modifier_in_notice_cb", "")
  784. weechat.hook_modifier("irc_in_privmsg", "fish_modifier_in_privmsg_cb", "")
  785. weechat.hook_modifier("irc_in_topic", "fish_modifier_in_topic_cb", "")
  786. weechat.hook_modifier("irc_in_332", "fish_modifier_in_332_cb", "")
  787. weechat.hook_modifier(
  788. "irc_out_privmsg", "fish_modifier_out_privmsg_cb", "")
  789. weechat.hook_modifier("irc_out_topic", "fish_modifier_out_topic_cb", "")
  790. elif (__name__ == "__main__" and len(sys.argv) == 3):
  791. key = sys.argv[1]
  792. msg = sys.argv[2]
  793. print(blowcrypt_unpack(msg, Blowfish(key), key))