FiSH encryption for X-Chat Python
2010-10-06 12:45:02
Post: #1
Vì bản plugin viết bằng C có vấn đề với Freenode nên mình viết lại một bản khác sử dụng Python cho tiện.

Bản này có hai tập tin chính là fish.pyirccrypt.py.

fish.py

###
#
# FiSH/Mircryption clone for X-Chat in 100% Python
#
# Requirements: PyCrypto, and Python 2.5+
#
# Copyright 2010 Nam T. Nguyen
# Released under the BSD license
#
# irccrypt module is copyright 2009 Bjorn Edstrom
# with modification from Nam T. Nguyen
#
###
from __future__ import with_statement
__module_name__ = 'fish'
__module_version__ = '1.0'
__module_description__ = 'fish encryption in pure python'
import irccrypt
import pickle
import os
import threading
KEY_MAP = {}
LOCK_MAP = {}
class SecretKey(object):
	def __init__(self, dh, key=None):
		self.dh = dh
		self.key = key
		self.cbc_mode = False
def set_processing():
	id = xchat.get_info('server')
	LOCK_MAP[id] = True
def unset_processing():
	id = xchat.get_info('server')
	LOCK_MAP[id] = False
	
def is_processing():
	id = xchat.get_info('server')
	return LOCK_MAP.get(id, False)
def get_id(ctx):
	return (ctx.get_info('server'), ctx.get_info('channel'))
def get_nick(full):
	if full[0] == ':':
		full = full[1 : ]
	return full[ : full.index('!')]
def unload(userdata):
	tmp_map = {}
	for id, key in KEY_MAP.iteritems():
		if key.key:
			tmp_map[id] = key
	with open(os.path.join(xchat.get_info('xchatdir'),
		'fish.pickle'), 'wb') as f:
		pickle.dump(tmp_map, f)
	print 'fish unloaded'
def decrypt(key, inp):
	decrypt_clz = irccrypt.Blowfish
	decrypt_func = irccrypt.blowcrypt_unpack
	if 3 <= inp.find(' *') <= 4:
		decrypt_clz = irccrypt.BlowfishCBC
		decrypt_func = irccrypt.mircryption_cbc_unpack
	b = decrypt_clz(key.key)
	return decrypt_func(inp, b)
def encrypt(key, inp):
	encrypt_clz = irccrypt.Blowfish
	encrypt_func = irccrypt.blowcrypt_pack
	if key.cbc_mode:
		encrypt_clz = irccrypt.BlowfishCBC
		encrypt_func = irccrypt.mircryption_cbc_pack
	b = encrypt_clz(key.key)
	return encrypt_func(inp, b)
def decrypt_print(word, word_eol, userdata):
	if is_processing():
		return xchat.EAT_NONE
	ctx = xchat.get_context()
	id = get_id(ctx)
	if id not in KEY_MAP:
		return xchat.EAT_NONE
	speaker, message = word[0], word_eol[1]
	# if there is mode char, remove it from the message
	if len(word_eol) >= 3:
		message = message[ : -(len(word_eol[2]) + 1)]
	if message.startswith('+OK ') or message.startswith('mcps '):
		message = decrypt(KEY_MAP[id], message)
		set_processing()
		ctx.emit_print(userdata, speaker, message)
		unset_processing()
		return xchat.EAT_XCHAT
	else:
		return xchat.EAT_NONE
def encrypt_privmsg(word, word_eol, userdata):
	message = word_eol[0]
	ctx = xchat.get_context()
	id = get_id(ctx)
	if id not in KEY_MAP:
		return xchat.EAT_NONE
	key = KEY_MAP[id]
	if not key.key:
		return xchat.EAT_NONE
	cipher = encrypt(key, message)
	xchat.command('PRIVMSG %s :%s' % (id[1], cipher))
	xchat.emit_print('Your Message', xchat.get_info('nick'), message)
	return xchat.EAT_ALL
def key(word, word_eol, userdata):
	ctx = xchat.get_context()
	target = ctx.get_info('channel')
	if len(word) >= 2:
		target = word[1]
	id = (ctx.get_info('server'), target)
	try:
		key = KEY_MAP[id]
	except KeyError:
		key = SecretKey(None)
	if len(word) >= 3:
		key.key = word_eol[2]
		KEY_MAP[id] = key
	print 'Key for', target, 'set to', key.key
	return xchat.EAT_ALL
def key_exchange(word, word_eol, userdata):
	ctx = xchat.get_context()
	target = ctx.get_info('channel')
	if len(word) >= 2:
		target = word[1]
	id = (ctx.get_info('server'), target)
	dh = irccrypt.DH1080Ctx()
	KEY_MAP[id] = SecretKey(dh)
	ctx.command('NOTICE %s %s' % (target, irccrypt.dh1080_pack(dh)))
	return xchat.EAT_ALL
def dh1080_finish(word, word_eol, userdata):
	ctx = xchat.get_context()
	speaker, command, target, message = word[0], word[1], word[2], word_eol[3]
	id = (ctx.get_info('server'), get_nick(speaker))
	if id not in KEY_MAP:
		return xchat.EAT_NONE
	key = KEY_MAP[id]
	irccrypt.dh1080_unpack(message[1 : ], key.dh)
	key.key = irccrypt.dh1080_secret(key.dh)
	print 'Key for', id[1], 'set to', key.key
	return xchat.EAT_ALL
def dh1080_init(word, word_eol, userdata):
	ctx = xchat.get_context()
	speaker, command, target, message = word[0], word[1], word[2], word_eol[3]
	id = (ctx.get_info('server'), get_nick(speaker))
	key = SecretKey(None)
	dh = irccrypt.DH1080Ctx()
	irccrypt.dh1080_unpack(message[1 : ], dh)
	key.key = irccrypt.dh1080_secret(dh)
	xchat.command('NOTICE %s %s' % (id[1], irccrypt.dh1080_pack(dh)))
	KEY_MAP[id] = key
	print 'Key for', id[1], 'set to', key.key
	return xchat.EAT_ALL
def dh1080(word, word_eol, userdata):
	if word_eol[3].startswith(':DH1080_FINISH'):
		return dh1080_finish(word, word_eol, userdata)
	elif word_eol[3].startswith(':DH1080_INIT'):
		return dh1080_init(word, word_eol, userdata)
	return xchat.EAT_NONE
def load():
	global KEY_MAP
	try:
		with open(os.path.join(xchat.get_info('xchatdir'),
			'fish.pickle'), 'rb') as f:
			KEY_MAP = pickle.load(f)
	except IOError:
		pass
	print 'fish loaded'
def key_list(word, word_eol, userdata):
	for id, key in KEY_MAP.iteritems():
		print id, key.key, bool(key.cbc_mode)
	return xchat.EAT_ALL
def key_remove(word, word_eol, userdata):
	id = (xchat.get_info('server'), word[1])
	try:
		del KEY_MAP[id]
	except KeyError:
		print 'Key not found'
	else:
		print 'Key removed'
	return xchat.EAT_ALL
def key_cbc(word, word_eol, userdata):
	id = (xchat.get_info('server'), word[1])
	try:
		KEY_MAP[id].cbc_mode = int(word[2])
		print 'CBC mode', bool(KEY_MAP[id].cbc_mode)
	except KeyError:
		print 'Key not found'
	return xchat.EAT_ALL
# handle topic line
def server_332(word, word_eol, userdata):
	if is_processing():
		return xchat.EAT_NONE
	id = get_id(xchat.get_context())
	if id not in KEY_MAP:
		return xchat.EAT_NONE
	key = KEY_MAP[id]
	server, cmd, nick, channel, topic = word[0], word[1], word[2], word[3], word_eol[4]
	if topic[0] == ':':
		topic = topic[1 : ]
	if not (topic.startswith('+OK ') or topic.startswith('mcps ')):
		return xchat.EAT_NONE
	topic = decrypt(key, topic)
	set_processing()
	xchat.command('RECV %s %s %s %s :%s' % (server, cmd, nick, channel, topic))
	unset_processing()
	return xchat.EAT_ALL
def change_nick(word, word_eol, userdata):
	old, new = word[0], word[1]
	ctx = xchat.get_context()
	old_id = (xchat.get_info('server'), old)
	new_id = (old_id[0], new)
	try:
		KEY_MAP[new_id] = KEY_MAP[old_id]
		del KEY_MAP[old_id]
	except KeyError:
		pass
	return xchat.EAT_NONE
import xchat
xchat.hook_command('key', key, help='show information or set key, /key <nick> [new_key]')
xchat.hook_command('key_exchange', key_exchange, help='exchange a new key, /key_exchange <nick>')
xchat.hook_command('key_list', key_list, help='list keys, /key_list')
xchat.hook_command('key_remove', key_remove, help='remove key, /key_remove <nick>')
xchat.hook_command('key_cbc', key_cbc, help='set cbc mode, /key_cbc <nick> <0|1>')
xchat.hook_server('notice', dh1080)
xchat.hook_print('Channel Message', decrypt_print, 'Channel Message')
xchat.hook_print('Change Nick', change_nick)
xchat.hook_print('Private Message to Dialog', decrypt_print, 'Private Message to Dialog')
xchat.hook_server('332', server_332)
xchat.hook_command('', encrypt_privmsg)
xchat.hook_unload(unload)
load()

irccrypt.py

#!/usr/bin/env python
##
## irccrypt.py - various cryptographic methods for IRC + IRCSRP reference
## implementation.
##
## 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.
## 
## THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
## WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
## MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
## ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
## WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
## ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
## OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
##
""" Library for various common cryptographic methods used on IRC.
Currently supports:
* blowcrypt - as used by for Fish et al.
* Mircryption-CBC - an improvement of blowcrypt using the CBC mode.
* DH1080 - A Diffie Hellman key exchange adapted for IRC usage.
Additionally, implements the new IRCSRP method as described at
http://www.bjrn.se/ircsrp
Sample usage:
blowcrypt, Fish etc
-------------------
>>> b = Blowfish("password")
>>> blowcrypt_pack("Hi bob!", b)
'+OK BRurM1bWPZ1.'
>>> blowcrypt_unpack(_, b)
'Hi bob!'
Mircryption-CBC
---------------
>>> b = BlowfishCBC("keyTest")
>>> mircryption_cbc_pack("12345678", b)
'+OK *oXql/CRQbadX+5kl68g1uQ=='
DH1080
------
>>> alice = DH1080Ctx()
>>> bob = DH1080Ctx()
>>> dh1080_pack(alice)
'DH1080_INIT qStH1LjBpb47s0XY80W9e3efrVSh2Qfq...<snip>
>>> dh1080_unpack(_, bob)
True
>>> dh1080_pack(bob)
'DH1080_FINISH mjyk//fqPoEwp5JfbJtzDmlfpzmtME...<snip>
>>> dh1080_unpack(_, alice)
True
>>> dh1080_secret(alice)
'tfu4Qysoy56OYeckat1HpJWzi+tJVx/cm+Svzb6eunQ'
>>> dh1080_secret(bob)
'tfu4Qysoy56OYeckat1HpJWzi+tJVx/cm+Svzb6eunQ'
For more information, see the accompanying article at http://www.bjrn.se/
"""
__author__ = "Bjorn Edstrom <be@bjrn.se>"
__date__ = "2009-01-25"
__version__ = "0.1.1"
import base64
import hashlib
from math import log
try:
    import Crypto.Cipher.Blowfish
    import Crypto.Cipher.AES
except ImportError:
    print "This module requires PyCrypto / The Python Cryptographic Toolkit."
    print "Get it from http://www.dlitz.net/software/pycrypto/."
    raise
from os import urandom
import struct
import time
##
## Preliminaries.
##
class MalformedError(Exception):
    pass
def sha256(s):
    """sha256"""
    return hashlib.sha256(s).digest()
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 bytes2int(b):
    """Variable length big endian to integer."""
    n = 0
    for p in b:
        n *= 256
        n += ord(p)
    return n
# FIXME! Only usable for really small a with b near 16^x.
def randint(a, b):
    """Random integer in [a,b]."""
    bits = int(log(b, 2) + 1) / 8
    candidate = 0
    while True:
        candidate = bytes2int(urandom(bits))
        if a <= candidate <= b:
            break
    assert a <= candidate <= b
    return candidate
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 xorstring(a, b, blocksize): # Slow.
    """xor string a and b, both of length blocksize."""
    xored = ''
    for i in xrange(blocksize):
        xored += chr(ord(a[i]) ^ ord(b[i]))  
    return xored
def cbc_encrypt(func, data, blocksize):
    """The CBC mode. The randomy generated IV is prefixed to the ciphertext.
    'func' is a function that encrypts data in ECB mode. 'data' is the
    plaintext. 'blocksize' is the block size of the cipher."""
    assert len(data) % blocksize == 0
    
    IV = urandom(blocksize)
    assert len(IV) == blocksize
    
    ciphertext = IV
    for block_index in xrange(len(data) / blocksize):
        xored = xorstring(data, IV, blocksize)
        enc = func(xored)
        
        ciphertext += enc
        IV = enc
        data = data[blocksize:]
    assert len(ciphertext) % blocksize == 0
    return ciphertext
def cbc_decrypt(func, data, blocksize):
    """See cbc_encrypt."""
    assert len(data) % blocksize == 0
    
    IV = data[0:blocksize]
    data = data[blocksize:]
    plaintext = ''
    for block_index in xrange(len(data) / blocksize):
        temp = func(data[0:blocksize])
        temp2 = xorstring(temp, IV, blocksize)
        plaintext += temp2
        IV = data[0:blocksize]
        data = data[blocksize:]
    
    assert len(plaintext) % blocksize == 0
    return plaintext
class Blowfish:
    
    def __init__(self, key=None):
        if key:
            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)
class BlowfishCBC:
    
    def __init__(self, key=None):
        if key:
            self.blowfish = Crypto.Cipher.Blowfish.new(key)
    def decrypt(self, data):
        return cbc_decrypt(self.blowfish.decrypt, data, 8)
    
    def encrypt(self, data):
        return cbc_encrypt(self.blowfish.encrypt, data, 8)
class AESCBC:
    
    def __init__(self, key):
        self.aes = Crypto.Cipher.AES.new(key)
    def decrypt(self, data):
        return cbc_decrypt(self.aes.decrypt, data, 16)
    
    def encrypt(self, data):
        return cbc_encrypt(self.aes.encrypt, data, 16)
##
## blowcrypt, Fish etc.
##
# 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 xrange(6):
            res += B64[right & 0x3f]
            right >>= 6
        for i in xrange(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)
        res += struct.pack('>LL', left, right)
        s = s[12:]
    return res
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
    try:
        raw = blowcrypt_b64decode(rest)
    except TypeError:
        raise MalformedError
    if not raw:
        raise MalformedError
    try:
        plain = cipher.decrypt(raw)
    except ValueError:
        raise MalformedError
    
    return plain.strip('\x00')
##
## Mircryption-CBC
##
def mircryption_cbc_pack(msg, cipher):
    """."""
    padded = padto(msg, 8)
    return '+OK *' + base64.b64encode(cipher.encrypt(padded))
def mircryption_cbc_unpack(msg, cipher):
    """."""
    if not (msg.startswith('+OK *') or msg.startswith('mcps *')):
        raise ValueError
    try:
        _, coded = msg.split('*', 1)
        raw = base64.b64decode(coded)
    except TypeError:
        raise MalformedError
    if not raw:
        raise MalformedError
    try:
        padded = cipher.decrypt(raw)
    except ValueError:
        raise MalformedError
    if not padded:
        raise MalformedError
    plain = padded.strip("\x00")
    return plain
##
## DH1080
##
g_dh1080 = 2
p_dh1080 = int('FBE1022E23D213E8ACFA9AE8B9DFAD'
               'A3EA6B7AC7A7B7E95AB5EB2DF85892'
               '1FEADE95E6AC7BE7DE6ADBAB8A783E'
               '7AF7A7FA6A2B7BEB1E72EAE2B72F9F'
               'A2BFB2A2EFBEFAC868BADB3E828FA8'
               'BADFADA3E4CC1BE7E8AFE85E9698A7'
               '83EB68FA07A77AB6AD7BEB618ACF9C'
               'A2897EB28A6189EFA07AB99A8A7FA9'
               'AE299EFA7BA66DEAFEFBEFBF0B7D8B', 16)
q_dh1080 = (p_dh1080 - 1) / 2 
# XXX: It is probably possible to implement dh1080 base64 using Pythons own, by
# considering padding, lengths etc. The dh1080 implementation is basically the
# standard one but with the padding character '=' removed. A trailing 'A'
# is also added sometimes.
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)))

Chép cả hai vào thư mục .xchat, hoặc tự nạp fish.py mỗi khi chạy X-Chat.

Một vài lệnh cơ bản
/key_exchange <nick>
Dùng để tạo khóa chung với một người nào đó. Sau khi có khóa này thì mọi trao đổi riêng (X-Chat gọi là Dialog) với người đó sẽ được mã hóa.
/key <nick> [new_key]
Nếu không nhập new_key thì sẽ hiện ra khóa chung đã được thiết lập với người này. Nếu nhập thêm new_key thì sẽ thiết lập khóa chung là new_key.
/key_remove <nick>
Xóa bỏ khóa chung với người đó.
/key_list
Liệt kê tất cả các khóa đã được thiết lập ở máy chủ hiện tại.

Chú ý: Tập tin fish.pickle sẽ được tạo ra trong thư mục .xchat. Tập tin này cần được bảo vệ vì nó chứa các khóa đã được thiết lập.
2011-04-26 15:01:13
Post: #2
Very good.

I tried it on Windows, however I wish you would fix few things.

When I entered some channels with fished topic I did get this error,
 Traceback (most recent call last):
File "C:\Documents and Settings\Admin\Application Data\X-Chat 2\fish.py", line 222, in server_332
topic = decrypt(key, topic)
File "C:\Documents and Settings\Admin\Application Data\X-Chat 2\fish.py", line 70, in decrypt
b = decrypt_clz(key.key)
File "C:\Documents and Settings\Admin\Application Data\X-Chat 2\irccrypt.py", line 197, in __init__
self.blowfish = Crypto.Cipher.Blowfish.new(key)
ValueError: Maximum key size is 448 bits
Maybe it would be better if key_exchange was called keyx for short and result would look more like,
FiSH: Received DH1080 public key from nickname, sending mine...
FiSH: Key for nickname successfully set!
instead of,
>nickname< DH1080_INIT ZIaexAmfPptdyNeS/Bc0kPUsw7g1pMKPoVykbyKX7si3hf1ADf3whCNk78VGfscdnlhGgtvct+TIpTWyIdORHmsbQiArLz/0KjU9CHr5Ox4wmd8n67pZxJNtvHPP1jXUJqv7fAVERX28cxVP9wSSpl3oJEhtKcDpXERa0FlmZLkjRCmGITkZA
Key for nickname set to Z0EX2Y84xfnKdHFdqHlPY63hGaMSSSS9J6enrdmZ/tE
Think I also noticed that file with stored keys was not saving to the disk.
2011-04-27 02:59:52
Post: #3
Hello,

1. It seems to be a bug. Have you set the FiSH key for that channel? Can you provide a raw log? As well as key for that channel?

2. Cosmetic change like this could be easily done by you, no? I'll make a change when I fix issue 1.

3. The key would be saved when you unload this plugin.

Thanks for trying it and reporting bugs :D.
2011-04-27 16:17:54
Post: #4
Sorry for possible lack of detail, I'm no expert.

I created a test channel with other client (running FiSH 0.98) and set the key to:
sdg54G54gdG44Gg1G415541G12G_Gdg54Ggg_G8d1ee8551Ggdssrgh545fs

Then I started other client with your plugin and set the key there.

If encrypted topic is set, I immediately get an error message (on join) posted above.

And everytime I try to send something from this client I get:
(message is not send)
 Traceback (most recent call last):
File "C:\Settings\Admin\Application Data\X-Chat 2\fish.py", line 111, in
encrypt_privmsg
cipher = encrypt(key, message)
File "C:\Settings\Admin\Application Data\X-Chat 2\fish.py", line 79, in encrypt
b = encrypt_clz(key.key)
File "C:\Settings\Admin\Application Data\X-Chat 2\irccrypt.py", line 197, in __init__
self.blowfish = Crypto.Cipher.Blowfish.new(key)
ValueError: Maximum key size is 448 bits
Seems like problem is simply size/length of used key, as error shows.

I tried to set key to something very simple, like 123456789 and I had no problem.

There was also no problem with automatic key exchange and private messages.

But for channels, many of them use keys of similar or bigger size, so “ValueError: Maximum key size is 448 bits” makes it unusable for it.


Big and great improvement is that coloring and other scripts finally works in fished channels (with your plugin).

But there is few other issues.

Higlighting doesnt work, neither for nickname or ‘extra words to highlight’.

Channel keys are saved per server, which is basically good, because it guarantess multi-network support for different channels with the same name, but it also separates servers of the same network so if I jump to another server, I have to set key for same channel on it.

for example:
('leguin.freenode.net', ‘#somechan’) 123456789 False
('holmes.freenode.net', ‘#somechan’) 123456789 False


Maybe little solution could be making optional server setting with wildcards support.
for example:
/key #somechan 123456789 *.freenode.net
This is only problem if someone often change servers or doesnt use bnc. But it happens.

It would be nice if there was an option to send unecrypted message without unloading plugin. It can be done in original FiSH plugin with plain_prefix=“+p ”

And lastly, plaintext keys in fish.pickle can be a problem, no matter how well ‘protected’ that file is.
Though I'm not sure if it is possible to do it without (as in FiSH 0.98) entering some ‘master key’ every time before loading of the plugin.

:)
2011-04-28 09:06:48
Post: #5
1. It's PyCrypto bug. See https://bugs.launchpad.net/pycrypto/+bug/695417. But then again, it's probably for the best. You should use shorter keys too. If it's not possible, please downgrade to PyCrypto 2.0.1.

2. I don't quite understand the “highlighting” problem. What is it?

3. I had exactly the reverse problem. I needed to chat with different people using the same nick name on differing servers. Your idea of a network mask could partially solve both your problem and mine. Or maybe an array of network masks. And then some sort of inclusion/exclusion method. And then ordering… But a network mask is a good start. I'll try to add one in.

4. I will also add an option to send unencrypted message.

5. That's true regarding the plaintext-ness of fish.pickle. But fixing this means auto-save-on-unload will stop working too. You don't want the plugin to store a copy of your encryption password in memory until it unloads, do you? Anyway, I'll think more about it.

Thanks.
2011-04-28 14:32:17
Post: #6
Correct, 2.0.1 works fine.

You can see highlight options here generally your message should turn red (default setting) if someone write your nickname or channel name in tab/tree view will turn blue (by default) if you are ‘highlighted’ while not being active in the channel window/tab. Now it works as if it was regular message. Its not that big deal, I read there is some problem with it here. By the way, message/channame highlight was one thing that actually worked in original FiSH (0.98) plugin (except highlightes for the ‘Extra words to highlight’, ‘Nick names not to highlight’, ‘Nick names to always highlight’ options, but thats almost negligible).

I'm not sure what would be the best method for handling keys, I just think storing them in plaintext is very vulnerable and thus kind of defies purpose of the encryption. Though its only bigger issue left in your plugin now.

Thanks for the effort.
2011-05-02 09:20:34
Post: #7
This version has some new features as outlined in the code.

Note that fish.pickle will be always automatically loaded. Keys will only be automatically dumped to fish.pickle if fish_secure.pickle does not exist.

To load from/dump to fish_secure.pickle, one must use /fish_load_secure and /fish_unload_secure.

During testing, I also noticed that direct use of /privmsg or /msg does not encrypt outgoing messages. Only messages in “Dialog Window” and “Channel” get encrypted.

Please try it. Thanks.

###
#
# FiSH/Mircryption clone for X-Chat in 100% Python
#
# Requirements: PyCrypto, and Python 2.5+
#
# Copyright 2011 Nam T. Nguyen
# Released under the BSD license
#
# irccrypt module is copyright 2009 Bjorn Edstrom
# with modification from Nam T. Nguyen
#
# Changelog:
#
#   * 2.0:
#      + Suport network mask in /key command
#      + Alias key_exchange to keyx
#      + Support plaintext marker '+p '
#      + Support encrypted key store
#
#   * 1.0:
#      + Initial release
#
###
from __future__ import with_statement
__module_name__ = 'fish'
__module_version__ = '2.0'
__module_description__ = 'fish encryption in pure python'
import irccrypt
import pickle
import os
import threading
PLAINTEXT_MARKER = '+p '
class KeyMap(dict):
	def __get_real_key(self, key):
		nick, server = (key[0], key[1].lower())
		# get all the keys for nick
		same_nick_keys = [k[1] for k in self.iterkeys() if k[0] == nick]
		# sort by network mask's length
		same_nick_keys.sort(key=lambda k: len(k), reverse=True)
		for k in same_nick_keys:
			if server.rfind(k) >= 0:
				return (nick, k)
	def __getitem__(self, key):
		return dict.__getitem__(self, self.__get_real_key(key))
	
	#def __setitem__(self, key, value):
	#	return dict.__setitem__(self, self.__get_real_key(key), value)
	def __contains__(self, key):
		return dict.__contains__(self, self.__get_real_key(key))
KEY_MAP = KeyMap()
LOCK_MAP = {}
class SecretKey(object):
	def __init__(self, dh, key=None):
		self.dh = dh
		self.key = key
		self.cbc_mode = False
def set_processing():
	id = xchat.get_info('server')
	LOCK_MAP[id] = True
def unset_processing():
	id = xchat.get_info('server')
	LOCK_MAP[id] = False
	
def is_processing():
	id = xchat.get_info('server')
	return LOCK_MAP.get(id, False)
def get_id(ctx):
	return (ctx.get_info('channel'), ctx.get_info('server'))
def get_nick(full):
	if full[0] == ':':
		full = full[1 : ]
	return full[ : full.index('!')]
def get_id_for(ctx, speaker):
	return (get_nick(speaker), ctx.get_info('server'))
def unload(userdata):
	tmp_map = KeyMap()
	encrypted_file = os.path.join(xchat.get_info('xchatdir'),
		'fish_secure.pickle')
	if os.path.exists(encrypted_file):
		return
	for id, key in KEY_MAP.iteritems():
		if key.key:
			tmp_map[id] = key
			key.dh = None
	with open(os.path.join(xchat.get_info('xchatdir'),
		'fish.pickle'), 'wb') as f:
		pickle.dump(tmp_map, f)
	print 'fish unloaded'
def decrypt(key, inp):
	decrypt_clz = irccrypt.Blowfish
	decrypt_func = irccrypt.blowcrypt_unpack
	if 3 <= inp.find(' *') <= 4:
		decrypt_clz = irccrypt.BlowfishCBC
		decrypt_func = irccrypt.mircryption_cbc_unpack
	b = decrypt_clz(key.key)
	return decrypt_func(inp, b)
def encrypt(key, inp):
	encrypt_clz = irccrypt.Blowfish
	encrypt_func = irccrypt.blowcrypt_pack
	if key.cbc_mode:
		encrypt_clz = irccrypt.BlowfishCBC
		encrypt_func = irccrypt.mircryption_cbc_pack
	b = encrypt_clz(key.key)
	return encrypt_func(inp, b)
def decrypt_print(word, word_eol, userdata):
	if is_processing():
		return xchat.EAT_NONE
	ctx = xchat.get_context()
	id = get_id(ctx)
	if id not in KEY_MAP:
		return xchat.EAT_NONE
	speaker, message = word[0], word_eol[1]
	# if there is mode char, remove it from the message
	if len(word_eol) >= 3:
		message = message[ : -(len(word_eol[2]) + 1)]
	if message.startswith('+OK ') or message.startswith('mcps '):
		message = decrypt(KEY_MAP[id], message)
		set_processing()
		ctx.emit_print(userdata, speaker, message)
		unset_processing()
		return xchat.EAT_XCHAT
	else:
		return xchat.EAT_NONE
def encrypt_privmsg(word, word_eol, userdata):
	message = word_eol[0]
	ctx = xchat.get_context()
	id = get_id(ctx)
	if id not in KEY_MAP:
		return xchat.EAT_NONE
	key = KEY_MAP[id]
	if not key.key or message.startswith(PLAINTEXT_MARKER):
		return xchat.EAT_NONE
	cipher = encrypt(key, message)
	xchat.command('PRIVMSG %s :%s' % (id[0], cipher))
	xchat.emit_print('Your Message', xchat.get_info('nick'), message)
	return xchat.EAT_ALL
def key(word, word_eol, userdata):
	ctx = xchat.get_context()
	target = ctx.get_info('channel')
	if len(word) >= 2:
		target = word[1]
	server = ctx.get_info('server')
	if len(word) >= 4:
		if word[2] == '--network':
			server = word[3]
	id = (target, server)
	try:
		key = KEY_MAP[id]
	except KeyError:
		key = SecretKey(None)
	if len(word) >= 3 and word[2] != '--network':
		key.key = word_eol[2]
		KEY_MAP[id] = key
	elif len(word) >= 5 and word[2] == '--network':
		key.key = word_eol[4]
		KEY_MAP[id] = key
	print 'Key for', id, 'set to', key.key
	return xchat.EAT_ALL
def key_exchange(word, word_eol, userdata):
	ctx = xchat.get_context()
	target = ctx.get_info('channel')
	if len(word) >= 2:
		target = word[1]
	id = (target, ctx.get_info('server'))
	dh = irccrypt.DH1080Ctx()
	KEY_MAP[id] = SecretKey(dh)
	ctx.command('NOTICE %s %s' % (target, irccrypt.dh1080_pack(dh)))
	return xchat.EAT_ALL
def dh1080_finish(word, word_eol, userdata):
	ctx = xchat.get_context()
	speaker, command, target, message = word[0], word[1], word[2], word_eol[3]
	id = get_id_for(ctx, speaker)
	print 'dh1080_finish', id
	if id not in KEY_MAP:
		return xchat.EAT_NONE
	key = KEY_MAP[id]
	irccrypt.dh1080_unpack(message[1 : ], key.dh)
	key.key = irccrypt.dh1080_secret(key.dh)
	print 'Key for', id[0], 'set to', key.key
	return xchat.EAT_ALL
def dh1080_init(word, word_eol, userdata):
	ctx = xchat.get_context()
	speaker, command, target, message = word[0], word[1], word[2], word_eol[3]
	id = get_id_for(ctx, speaker)
	key = SecretKey(None)
	dh = irccrypt.DH1080Ctx()
	irccrypt.dh1080_unpack(message[1 : ], dh)
	key.key = irccrypt.dh1080_secret(dh)
	xchat.command('NOTICE %s %s' % (id[0], irccrypt.dh1080_pack(dh)))
	KEY_MAP[id] = key
	print 'Key for', id[0], 'set to', key.key
	return xchat.EAT_ALL
def dh1080(word, word_eol, userdata):
	if word_eol[3].startswith(':DH1080_FINISH'):
		return dh1080_finish(word, word_eol, userdata)
	elif word_eol[3].startswith(':DH1080_INIT'):
		return dh1080_init(word, word_eol, userdata)
	return xchat.EAT_NONE
def load():
	global KEY_MAP
	try:
		with open(os.path.join(xchat.get_info('xchatdir'),
			'fish.pickle'), 'rb') as f:
			KEY_MAP = pickle.load(f)
	except IOError:
		pass
	print 'fish loaded'
def fish_unload_secure(word, word_eol, userdata):
	global KEY_MAP
	decrypted = pickle.dumps(KEY_MAP)
	algo = irccrypt.BlowfishCBC(word_eol[1])
	encrypted = irccrypt.mircryption_cbc_pack(decrypted, algo)
	try:
		with open(os.path.join(xchat.get_info('xchatdir'),
			'fish_secure.pickle'), 'wb') as f:
			f.write(encrypted)
	except IOError:
		pass
	print len(KEY_MAP), 'secure key(s) dumped'
	return xchat.EAT_ALL
def fish_load_secure(word, word_eol, userdata):
	global KEY_MAP
	try:
		with open(os.path.join(xchat.get_info('xchatdir'),
			'fish_secure.pickle'), 'rb') as f:
			encrypted = f.read()
	except IOError:
		pass
	algo = irccrypt.BlowfishCBC(word_eol[1])
	try:
		decrypted = irccrypt.mircryption_cbc_unpack(encrypted, algo)
		tmp_map = pickle.loads(decrypted)
	except:
		tmp_map = {}
	KEY_MAP.update(tmp_map)
	print len(tmp_map), 'secure key(s) loaded'
	return xchat.EAT_ALL
def key_list(word, word_eol, userdata):
	print 'Found', len(KEY_MAP), 'key(s)'
	for id, key in KEY_MAP.iteritems():
		print id, key.key, bool(key.cbc_mode)
	return xchat.EAT_ALL
def key_remove(word, word_eol, userdata):
	id = (word[1], xchat.get_info('server'))
	if id not in KEY_MAP and len(word) > 2:
		id = (word[1], word[2])
	try:
		del KEY_MAP[id]
	except KeyError:
		print 'Key not found'
	else:
		print 'Key removed'
	return xchat.EAT_ALL
def key_cbc(word, word_eol, userdata):
	id = (word[1], xchat.get_info('server'))
	try:
		KEY_MAP[id].cbc_mode = int(word[2])
		print 'CBC mode', bool(KEY_MAP[id].cbc_mode)
	except KeyError:
		print 'Key not found'
	return xchat.EAT_ALL
# handle topic line
def server_332(word, word_eol, userdata):
	if is_processing():
		return xchat.EAT_NONE
	id = get_id(xchat.get_context())
	if id not in KEY_MAP:
		return xchat.EAT_NONE
	key = KEY_MAP[id]
	server, cmd, nick, channel, topic = word[0], word[1], word[2], word[3], word_eol[4]
	if topic[0] == ':':
		topic = topic[1 : ]
	if not (topic.startswith('+OK ') or topic.startswith('mcps ')):
		return xchat.EAT_NONE
	topic = decrypt(key, topic)
	set_processing()
	xchat.command('RECV %s %s %s %s :%s' % (server, cmd, nick, channel, topic))
	unset_processing()
	return xchat.EAT_ALL
def change_nick(word, word_eol, userdata):
	old, new = word[0], word[1]
	ctx = xchat.get_context()
	old_id = (old, xchat.get_info('server'))
	new_id = (new, xchat.get_info('server'))
	try:
		KEY_MAP[new_id] = KEY_MAP[old_id]
		del KEY_MAP[old_id]
	except KeyError:
		pass
	return xchat.EAT_NONE
import xchat
xchat.hook_command('key', key, help='show information or set key, /key <nick> [<--network> <network>] [new_key]')
xchat.hook_command('key_exchange', key_exchange, help='exchange a new key, /key_exchange <nick>')
xchat.hook_command('keyx', key_exchange, help='exchange a new key, /keyx <nick>')
xchat.hook_command('key_list', key_list, help='list keys, /key_list')
xchat.hook_command('key_remove', key_remove, help='remove key, /key_remove <nick>')
xchat.hook_command('key_cbc', key_cbc, help='set cbc mode, /key_cbc <nick> <0|1>')
xchat.hook_command('fish_load_secure', fish_load_secure, help='load fish_secure.pickle, /fish_load_secure <passphrase>')
xchat.hook_command('fish_unload_secure', fish_unload_secure, help='dump fish_secure.pickle, /fish_unload_secure <passphrase>')
xchat.hook_server('notice', dh1080)
xchat.hook_print('Channel Message', decrypt_print, 'Channel Message')
xchat.hook_print('Change Nick', change_nick)
xchat.hook_print('Private Message to Dialog', decrypt_print, 'Private Message to Dialog')
xchat.hook_server('332', server_332)
xchat.hook_command('', encrypt_privmsg)
xchat.hook_unload(unload)
load()
2011-05-04 02:05:30
Post: #8
Hello

Some minor thing first, sometimes I get this warning/message on key exchange,
DH1080_INIT 
eyL1jDQnviBYeEZPjiQoJgFPu7paN26SvbeqmEXM+iZW7VXRMwTWVv3/7zztz9gDC/YPE5a+gJGtK0sDePCMYkX0b3CfKUqahO3Hel1L/FtqLYXfGzm+mOaWw3mAD3FLJ1uiPVxszeZttaDf2yPKoXGTDdnstw1ID8/MVdtDWV5/P6ih6b2A
dh1080_finish ('nickname', 'holmes.freenode.net')
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.
Key for nickname set to /w/1nHLE0V28Pdfi8/IEWsJekJnCm9mhKJ+8iZeCEzk
But it works nonetheless.


Now some more serious issues.
I get this error on /fish_unload_secure
 Traceback (most recent call last):
File "C:\Settings\Admin\Application Data\X-Chat 2\fish.py", line 238, in fish_unload_secure
algo = irccrypt.BlowfishCBC(word_eol[1])
IndexError: list index out of range
and on /fish_load_secure
 Traceback (most recent call last):
File "C:\Settings\Admin\Application Data\X-Chat 2\fish.py", line 257, in fish_load_secure
algo = irccrypt.BlowfishCBC(word_eol[1])
IndexError: list index out of range
I think I might be missing something?


Then I tried network mask but couldnt get it to work either.
Without network mask (working),
/key #testchan somekey
Key for ('#testchan', ‘holmes.freenode.net’) set to somekey
/key_remove #testchan
Key removed
With network mask,
/key #testchan –network *.freenode.net somekey
Key for ('#testchan', '*.freenode.net') set to somekey
but key is not set, from other client I receive messages in +OK 9D8Y41 format and outgoing messages are in plaintext.

Then if I want to remove this key,
/key_remove #testchan
Key not found
I have to do,
/key_remove #testchan *.freenode.net
Key removed
If I dont remove key but re-start I see,
/key_list
Found 1 key(s)
('#testchan', 'holmes.freenode.net') somekey
And again if I want to remove it,
/key_remove #testchan
Key not found
/key_remove #testchan holmes.freenode.net
Key removed
Ideally, when is it working properly, would be if you could ‘enable’ chan ‘globally’, like this /key #testchan –network * somekey

Support plaintext marker ‘+p ’
Can you make it so it doesnt prepend ‘+p ’ in an actual message?

During testing, I also noticed that direct use of /privmsg or /msg does not encrypt outgoing messages.
Yes, in FiSH 0.98 it is ‘/msg’ for plaintext, ‘/msg+’ for encrypted messages.
2011-05-04 09:17:20
Post: #9
1. That key exchange error message is ignorable.

2. /fish_unload_secure and /fish_load_secure take in a password. Use something like /fish_load_secure iamFAT.

3. The network mask is simpler, .freenode.net (without the asterisk). The matching algorithm is longest suffix matching. For example, if you have freenode.net and card.freenode.net, then card will be matched first, then the other. To enable it globally, use wide enough mask such as .com or .net.

4. Please bear with the ‘+p ’, or you'll loose highlighting, etc. Otherwise, please edit this
	if not key.key or message.startswith(PLAINTEXT_MARKER):
		return xchat.EAT_NONE
	cipher = encrypt(key, message)
to
	if not key.key:
		return xchat.EAT_NONE
	if not message.startswith(PLAINTEXT_MARKER):
		cipher = encrypt(key, message)
	else:
		cipher = message[3 : ]
2011-05-05 00:54:29
Post: #10
Ah right :)

Of course, there need to be a key/password :) (those error messages might be little misleading, but it is as you said before only cosmetic thing..)

There was no problem with dumping/loading the keys from what I tested.

Network masks also seemed to work fine.

Please bear with the ‘+p ’, or you'll loose highlighting, etc. Otherwise, please edit this
Im not sure what you meant here. I edited the code so it works as I wanted it to (without ‘+p ’ prefix in a message), but ‘highlighting’ did not work either way (for encrypted msgs).

As I saw it highlighting remained last functionality issue with a fish plugin. Which is quite interesting, because as I mentioned before, this thing somehow worked in otherwise broken FiSH 0.98 :) hehe.

Anyways, great job so far. :)

And this is probably just a specific problem for me, but I wonder if there is a way how to load/enter fish_secure.pickle password ‘on startup’ before XChat start to connect to 'auto connect on startup' networks. With the original FiSH 0.98, after plugin is loaded, it halts XChat until password is entered and then continues and connects to given irc networks. Reason for this is that with use of irc bouncer you will get older buffered messages in an encrypted format (if you dont load your keys before connecting, thats it) and also generally I think its better to be prompted to load your keys on program start.

Another ‘cosmetic’ but useful thing from FiSH 0.98 is that it will let you know on join, which channels will be un/encrypted,
FiSH: Chat in #channel will NOT be encrypted (no key found)
Now talking on #channel
Topic for #channel is: …..
New Reply
Related threads
Chào bạn. Muốn chạy 1 file code python thì có thể chạy bằng command: c:\python27\python path\your_file.py (giả sử bạn cài python 2.7 trên ổ C) Còn muốn chạy project bạn download về thì bạn phải xem document của nó
Công ty mình đang tuyển python developer, Part-time và Full-time. Các bạn vào đây xem nhé: http://www.vietnamworks.com/python-developer-388446-jd
Em định làm một bài nho nhỏ giải quyết sudoku trong python. Trước hết là đối với thuật toán cơ bản, mong các đại ca làm một tut hướng dẫn để học hỏi các kĩ thuật cần thiết trong python luôn. Thanks trước :)
VCCorp tuyển Python Software Engineer và Python Web Developer Thông tin chi tiết tại: [url]http://www.vccorp.vn/tuyen-dung/job-163/python-developer.htm[/url] Giới thiệu tổng quan về VCCorp: [url]http://www.vccorp.vn/gioi-thieu-vc.htm[/url]
Công ty mình cần tuyển 2 vị trí senior python/django developer. Yêu cầu: + Ít nhất 1 năm kinh nghiệm với python/django + Có trách nhiệm cao trong công việc + Chăm chỉ và sáng tạo Đãi ngộ: + Các chế độ xã hội theo luật pháp việt nam + Mức lương hấp dẫn + Môi trường làm việc thoải mái, thú


Statistic

Our users have posted a total of 694 posts | We have 464 registered users

Homepage | Tutorial | Return to Top |