#!/usr/bin/env python ## ## irccrypt.py - various cryptographic methods for IRC + IRCSRP reference ## implementation. ## ## Copyright (c) 2009, Bjorn Edstrom ## ## Permission to use, copy, modify, and distribute this software for any ## purpose with or without fee is hereby granted, provided that the above ## copyright notice and this permission notice appear in all copies. ## ## 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... >>> dh1080_unpack(_, bob) True >>> dh1080_pack(bob) 'DH1080_FINISH mjyk//fqPoEwp5JfbJtzDmlfpzmtME... >>> 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 " __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(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') ## ## 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 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 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))) ## ## IRCSRP version 1 ## modp14 = """ FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AACAA68 FFFFFFFF FFFFFFFF """ g = 2 N = int(modp14.replace(' ', '').replace('\n', ''), 16) H = sha256 class IRCSRPExchange: def __init__(self): self.status = 0 self.I = 0 self.x = 0 self.a = 0 self.A = 0 self.b = 0 self.B = 0 self.S = 0 self.u = 0 self.K = 0 self.M1 = 0 self.M2 = 0 class IRCSRPUsers: def __init__(self): # Store info about friends here, such as # self.db["alice"] = alice_s, alice_v self.db = {} # Temporary storage for exchange. The dict key is derived from the # IRC message, not the username. self.others = {} def get_details(self, username): s, v = self.db[username] return s, v class IRCSRPCtx: """Everyone has one of these.""" def __init__(self, dave=False): self.cipher = None self.status = 0 self.username = '' self.password = '' self.sessionkey = '' self.ex = IRCSRPExchange() self.isdave = dave if dave: self.users = IRCSRPUsers() def set_key(self, key): assert len(key) == 32 self.sessionkey = key self.cipher = AESCBC(key) if self.isdave: padded = padto('\xffKEY' + key, 16) return '+aes ' + base64.b64encode(self.cipher.encrypt(padded)) return None def ircsrp_generate(username, password): """Alice runs this and gives the result to Dave.""" s = urandom(32) x = bytes2int(H(s + username + password)) v = pow(g, x, N) return s, v def ircsrp_pack(ctx, msg): """Encrypt message for channel.""" times = struct.pack(">L", int(time.time())) infos = chr(len(ctx.username)) + ctx.username + times padded = padto('M' + infos + msg, 16) return '+aes ' + base64.b64encode(ctx.cipher.encrypt(padded)) def ircsrp_unpack(ctx, msg): """Decrypt message for channel.""" if not msg.startswith('+aes '): raise ValueError try: _, coded = msg.split(' ', 1) raw = base64.b64decode(coded) except TypeError: raise MalformedError if not raw: raise MalformedError try: padded = ctx.cipher.decrypt(raw) except ValueError: raise MalformedError if not padded: raise MalformedError plain = padded.strip("\x00") # New key? if plain.startswith('\xffKEY'): new = plain[4:] if not len(new) == 32: raise MalformedError ctx.sessionkey = new ctx.cipher = AESCBC(new) print "*** Session key changed to:", repr(new) return None if not plain[0] == 'M': raise ValueError usernamelen = ord(plain[1]) username = plain[2:2+usernamelen] timestampstr = plain[2+usernamelen:4+2+usernamelen] timestamp = struct.unpack(">L", timestampstr)[0] print "*** Sent by username:", username print "*** Sent at time:", time.ctime(timestamp) return plain[4+2+usernamelen:] def ircsrp_exchange(ctx, msg=None, sender=None): """The key exchange, for NOTICE handler. Parameters are: :!user@host.com NOTICE :\r\n """ b64 = lambda s: base64.b64encode(s) b64i = lambda i: b64(int2bytes(i)) unb64 = lambda s: base64.b64decode(s) unb64i = lambda s: bytes2int(unb64(s)) cmd, arg = '', '' if msg: cmd, arg = msg.split(' ', 1) if not cmd.startswith('+srp'): raise ValueError cmd = cmd[5:].strip(' ') # Alice initiates the exchange. if msg == None and sender == None and ctx.ex.status == 0: ctx.ex.status = 1 return "+srpa0 " + ctx.username # Dave if cmd == '0': ex = ctx.users.others[sender] = IRCSRPExchange() I = ex.I = arg s, v = ex.s, ex.v = ctx.users.get_details(I) b = ex.b = randint(2, N-1) B = ex.B = (3*v + pow(g, b, N)) % N return "+srpa1 " + b64(s + int2bytes(B)) # Alice if cmd == '1' and ctx.ex.status == 1: args = unb64(arg) s = ctx.ex.s = args[:32] B = ctx.ex.B = bytes2int(args[32:]) if B % N == 0: raise ValueError a = ctx.ex.a = randint(2, N-1) A = ctx.ex.A = pow(g, a, N) x = ctx.ex.x = bytes2int(H(s + ctx.username + ctx.password)) u = ctx.ex.u = bytes2int(H(int2bytes(A) + int2bytes(B))) S = ctx.ex.S = pow(B - 3*pow(g, x, N), (a + u*x) % N, N) K = ctx.ex.K = H(int2bytes(S)) M1 = ctx.ex.M1 = H(int2bytes(A) + int2bytes(B) + int2bytes(S)) ctx.ex.status = 2 return "+srpa2 " + b64(M1 + int2bytes(A)) # Dave if cmd == '2': if not sender in ctx.users.others: raise ValueError ex = ctx.users.others[sender] args = unb64(arg) M1 = args[:32] A = bytes2int(args[32:]) if A % N == 0: raise ValueError u = bytes2int(H(int2bytes(A) + int2bytes(ex.B))) S = pow(A * pow(ex.v, u, N), ex.b, N) K = H(int2bytes(S)) M2 = H(int2bytes(A) + M1 + int2bytes(S)) M1ver = H(int2bytes(A) + int2bytes(ex.B) + int2bytes(S)) if M1 != M1ver: raise ValueError aes = AESCBC(K) del ctx.users.others[sender] return "+srpa3 " + b64(aes.encrypt(ctx.sessionkey + M2)) # Alice if cmd == '3' and ctx.ex.status == 2: cipher = unb64(arg) aes = AESCBC(ctx.ex.K) plain = aes.decrypt(cipher) sessionkey = plain[:32] M2 = plain[32:] M2ver = H(int2bytes(ctx.ex.A) + ctx.ex.M1 + int2bytes(ctx.ex.S)) if M2 != M2ver: raise ValueError ctx.sessionkey = sessionkey ctx.cipher = AESCBC(sessionkey) print "*** Session key is:", repr(sessionkey) ctx.ex.status = 0 return True raise ValueError # 2*37*22312747