From: Chris Porter Date: Mon, 10 Nov 2008 21:58:39 +0000 (+0000) Subject: Add QTicket support! X-Git-Url: https://jfr.im/git/irc/quakenet/qwebirc.git/commitdiff_plain/ace37679e071504b32395bb6a91b7773f1e050b0 Add QTicket support! --- diff --git a/config.py.example b/config.py.example index 1c02532..ab7f7d4 100644 --- a/config.py.example +++ b/config.py.example @@ -7,3 +7,5 @@ MAXSUBSCRIPTIONS = 3 REALNAME = "http://moo.com/" MAXLINELEN = 600 AUTHGATEDOMAIN = "webchat_test" +QTICKETKEY = "boo" +QBOT = "Q@CServe.quakenet.org" diff --git a/qwebirc/ajaxengine.py b/qwebirc/ajaxengine.py index 0aae808..336feb2 100644 --- a/qwebirc/ajaxengine.py +++ b/qwebirc/ajaxengine.py @@ -1,7 +1,7 @@ from twisted.web import resource, server, static from twisted.names import client from twisted.internet import reactor -from authgateengine import login_optional +from authgateengine import login_optional, getSessionData import simplejson, md5, sys, os, ircclient, time, config, weakref, traceback Sessions = {} @@ -171,7 +171,13 @@ class AJAXEngine(resource.Resource): session = IRCSession(id) - client = ircclient.createIRC(session, nick=nick, ident=ident, ip=ip, realname=realname) + qticket = getSessionData(request).get("qticket") + if qticket is None: + perform = None + else: + perform = ["PRIVMSG %s :TICKETAUTH %s" % (config.QBOT, qticket)] + + client = ircclient.createIRC(session, nick=nick, ident=ident, ip=ip, realname=realname, perform=perform) session.client = client Sessions[id] = session diff --git a/qwebirc/authgateengine.py b/qwebirc/authgateengine.py index f412064..8765bc8 100644 --- a/qwebirc/authgateengine.py +++ b/qwebirc/authgateengine.py @@ -1,6 +1,8 @@ from authgate import twisted as authgate from twisted.web import resource, server, static -import config, urlparse, urllib +import config, urlparse, urllib, rijndael, ciphers, hashlib, re + +BLOCK_SIZE = 128/8 class AuthgateEngine(resource.Resource): isLeaf = True @@ -24,6 +26,10 @@ class AuthgateEngine(resource.Resource): # only used for informational purposes, the backend stores this seperately # so if the user changes it just their front end will be messed up! request.addCookie("user", ticket.username, path="/") + + qt = ticket.get("qticket") + if not qt is None: + getSessionData(request)["qticket"] = decodeQTicket(qt) location = request.getCookie("redirect") if location is None: @@ -38,6 +44,28 @@ class AuthgateEngine(resource.Resource): return server.NOT_DONE_YET +def decodeQTicket(qticket, p=re.compile("\x00*$"), cipher=rijndael.rijndael(hashlib.sha256(config.QTICKETKEY).digest()[:16])): + def decrypt(data): + l = len(data) + if l < BLOCK_SIZE * 2 or l % BLOCK_SIZE != 0: + raise Exception("Bad qticket.") + + iv, data = data[:16], data[16:] + cbc = ciphers.CBC(cipher, iv) + + # technically this is a flawed padding algorithm as it allows chopping at BLOCK_SIZE, we don't + # care about that though! + b = range(0, l-BLOCK_SIZE, BLOCK_SIZE) + for i, v in enumerate(b): + q = cbc.decrypt(data[v:v+BLOCK_SIZE]) + if i == len(b) - 1: + print repr(q), re.sub(p, "", q) + yield re.sub(p, "", q) + else: + print repr(q) + yield q + return "".join(decrypt(qticket)) + def getSessionData(request): return authgate.get_session_data(request) diff --git a/qwebirc/ciphers.py b/qwebirc/ciphers.py new file mode 100644 index 0000000..1593129 --- /dev/null +++ b/qwebirc/ciphers.py @@ -0,0 +1,38 @@ +# write me + +def xor(a, b): + assert(len(a) == len(b)) + out = [] + for i in range(0, len(a)): + out.append(chr(ord(a[i]) ^ ord(b[i]))) + + return "".join(out) + +class CBC: + def __init__(self, cipher, iv): + self.__cipher = cipher + self.__prevblock = False + self.__iv = iv + + def encrypt(self, block): + if not self.__prevblock: + i = self.__iv + else: + i = self.__prevblock + + c = xor(block, i) + + self.__prevblock = self.__cipher.encrypt(c) + return self.__prevblock + + def decrypt(self, block): + c = self.__cipher.decrypt(block) + if not self.__prevblock: + i = self.__iv + else: + i = self.__prevblock + + c = xor(c, i) + + self.__prevblock = block + return c diff --git a/qwebirc/ircclient.py b/qwebirc/ircclient.py index 1e85ea4..e7475d8 100644 --- a/qwebirc/ircclient.py +++ b/qwebirc/ircclient.py @@ -31,7 +31,11 @@ class QWebIRCClient(basic.LineReceiver): self.handleCommand(command, prefix, params) except irc.IRCBadMessage: self.badMessage(line, *sys.exc_info()) - + + if command == "001" and self.__perform: + for x in self.__perform: + self.write(x) + def badMessage(self, args): self("badmessage", args) @@ -50,6 +54,7 @@ class QWebIRCClient(basic.LineReceiver): self.lastError = None f = self.factory.ircinit nick, ident, ip, realname = f["nick"], f["ident"], f["ip"], f["realname"] + self.__perform = f.get("perform") hmac = hmacfn(ident, ip) self.write("USER %s bleh bleh %s %s :%s" % (ident, ip, hmac, realname)) diff --git a/qwebirc/rijndael.py b/qwebirc/rijndael.py new file mode 100644 index 0000000..fa025c6 --- /dev/null +++ b/qwebirc/rijndael.py @@ -0,0 +1,376 @@ +""" +A pure python (slow) implementation of rijndael with a decent interface + +To include - + +from rijndael import rijndael + +To do a key setup - + +r = rijndael(key, block_size = 16) + +key must be a string of length 16, 24, or 32 +blocksize must be 16, 24, or 32. Default is 16 + +To use - + +ciphertext = r.encrypt(plaintext) +plaintext = r.decrypt(ciphertext) + +If any strings are of the wrong length a ValueError is thrown +""" + +# ported from the Java reference code by Bram Cohen, April 2001 +# this code is public domain, unless someone makes +# an intellectual property claim against the reference +# code, in which case it can be made public domain by +# deleting all the comments and renaming all the variables + +import copy +import string + +shifts = [[[0, 0], [1, 3], [2, 2], [3, 1]], + [[0, 0], [1, 5], [2, 4], [3, 3]], + [[0, 0], [1, 7], [3, 5], [4, 4]]] + +# [keysize][block_size] +num_rounds = {16: {16: 10, 24: 12, 32: 14}, 24: {16: 12, 24: 12, 32: 14}, 32: {16: 14, 24: 14, 32: 14}} + +A = [[1, 1, 1, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1], + [1, 0, 0, 0, 1, 1, 1, 1], + [1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 1, 0, 0, 0, 1, 1], + [1, 1, 1, 1, 0, 0, 0, 1]] + +# produce log and alog tables, needed for multiplying in the +# field GF(2^m) (generator = 3) +alog = [1] +for i in xrange(255): + j = (alog[-1] << 1) ^ alog[-1] + if j & 0x100 != 0: + j ^= 0x11B + alog.append(j) + +log = [0] * 256 +for i in xrange(1, 255): + log[alog[i]] = i + +# multiply two elements of GF(2^m) +def mul(a, b): + if a == 0 or b == 0: + return 0 + return alog[(log[a & 0xFF] + log[b & 0xFF]) % 255] + +# substitution box based on F^{-1}(x) +box = [[0] * 8 for i in xrange(256)] +box[1][7] = 1 +for i in xrange(2, 256): + j = alog[255 - log[i]] + for t in xrange(8): + box[i][t] = (j >> (7 - t)) & 0x01 + +B = [0, 1, 1, 0, 0, 0, 1, 1] + +# affine transform: box[i] <- B + A*box[i] +cox = [[0] * 8 for i in xrange(256)] +for i in xrange(256): + for t in xrange(8): + cox[i][t] = B[t] + for j in xrange(8): + cox[i][t] ^= A[t][j] * box[i][j] + +# S-boxes and inverse S-boxes +S = [0] * 256 +Si = [0] * 256 +for i in xrange(256): + S[i] = cox[i][0] << 7 + for t in xrange(1, 8): + S[i] ^= cox[i][t] << (7-t) + Si[S[i] & 0xFF] = i + +# T-boxes +G = [[2, 1, 1, 3], + [3, 2, 1, 1], + [1, 3, 2, 1], + [1, 1, 3, 2]] + +AA = [[0] * 8 for i in xrange(4)] + +for i in xrange(4): + for j in xrange(4): + AA[i][j] = G[i][j] + AA[i][i+4] = 1 + +for i in xrange(4): + pivot = AA[i][i] + if pivot == 0: + t = i + 1 + while AA[t][i] == 0 and t < 4: + t += 1 + assert t != 4, 'G matrix must be invertible' + for j in xrange(8): + AA[i][j], AA[t][j] = AA[t][j], AA[i][j] + pivot = AA[i][i] + for j in xrange(8): + if AA[i][j] != 0: + AA[i][j] = alog[(255 + log[AA[i][j] & 0xFF] - log[pivot & 0xFF]) % 255] + for t in xrange(4): + if i != t: + for j in xrange(i+1, 8): + AA[t][j] ^= mul(AA[i][j], AA[t][i]) + AA[t][i] = 0 + +iG = [[0] * 4 for i in xrange(4)] + +for i in xrange(4): + for j in xrange(4): + iG[i][j] = AA[i][j + 4] + +def mul4(a, bs): + if a == 0: + return 0 + r = 0 + for b in bs: + r <<= 8 + if b != 0: + r = r | mul(a, b) + return r + +T1 = [] +T2 = [] +T3 = [] +T4 = [] +T5 = [] +T6 = [] +T7 = [] +T8 = [] +U1 = [] +U2 = [] +U3 = [] +U4 = [] + +for t in xrange(256): + s = S[t] + T1.append(mul4(s, G[0])) + T2.append(mul4(s, G[1])) + T3.append(mul4(s, G[2])) + T4.append(mul4(s, G[3])) + + s = Si[t] + T5.append(mul4(s, iG[0])) + T6.append(mul4(s, iG[1])) + T7.append(mul4(s, iG[2])) + T8.append(mul4(s, iG[3])) + + U1.append(mul4(t, iG[0])) + U2.append(mul4(t, iG[1])) + U3.append(mul4(t, iG[2])) + U4.append(mul4(t, iG[3])) + +# round constants +rcon = [1] +r = 1 +for t in xrange(1, 30): + r = mul(2, r) + rcon.append(r) + +del A +del AA +del pivot +del B +del G +del box +del log +del alog +del i +del j +del r +del s +del t +del mul +del mul4 +del cox +del iG + +class rijndael: + def __init__(self, key, block_size = 16): + if block_size != 16 and block_size != 24 and block_size != 32: + raise ValueError('Invalid block size: ' + str(block_size)) + if len(key) != 16 and len(key) != 24 and len(key) != 32: + raise ValueError('Invalid key size: ' + str(len(key))) + self.block_size = block_size + + ROUNDS = num_rounds[len(key)][block_size] + BC = block_size / 4 + # encryption round keys + Ke = [[0] * BC for i in xrange(ROUNDS + 1)] + # decryption round keys + Kd = [[0] * BC for i in xrange(ROUNDS + 1)] + ROUND_KEY_COUNT = (ROUNDS + 1) * BC + KC = len(key) / 4 + + # copy user material bytes into temporary ints + tk = [] + for i in xrange(0, KC): + tk.append((ord(key[i * 4]) << 24) | (ord(key[i * 4 + 1]) << 16) | + (ord(key[i * 4 + 2]) << 8) | ord(key[i * 4 + 3])) + + # copy values into round key arrays + t = 0 + j = 0 + while j < KC and t < ROUND_KEY_COUNT: + Ke[t / BC][t % BC] = tk[j] + Kd[ROUNDS - (t / BC)][t % BC] = tk[j] + j += 1 + t += 1 + tt = 0 + rconpointer = 0 + while t < ROUND_KEY_COUNT: + # extrapolate using phi (the round key evolution function) + tt = tk[KC - 1] + tk[0] ^= (S[(tt >> 16) & 0xFF] & 0xFF) << 24 ^ \ + (S[(tt >> 8) & 0xFF] & 0xFF) << 16 ^ \ + (S[ tt & 0xFF] & 0xFF) << 8 ^ \ + (S[(tt >> 24) & 0xFF] & 0xFF) ^ \ + (rcon[rconpointer] & 0xFF) << 24 + rconpointer += 1 + if KC != 8: + for i in xrange(1, KC): + tk[i] ^= tk[i-1] + else: + for i in xrange(1, KC / 2): + tk[i] ^= tk[i-1] + tt = tk[KC / 2 - 1] + tk[KC / 2] ^= (S[ tt & 0xFF] & 0xFF) ^ \ + (S[(tt >> 8) & 0xFF] & 0xFF) << 8 ^ \ + (S[(tt >> 16) & 0xFF] & 0xFF) << 16 ^ \ + (S[(tt >> 24) & 0xFF] & 0xFF) << 24 + for i in xrange(KC / 2 + 1, KC): + tk[i] ^= tk[i-1] + # copy values into round key arrays + j = 0 + while j < KC and t < ROUND_KEY_COUNT: + Ke[t / BC][t % BC] = tk[j] + Kd[ROUNDS - (t / BC)][t % BC] = tk[j] + j += 1 + t += 1 + # inverse MixColumn where needed + for r in xrange(1, ROUNDS): + for j in xrange(BC): + tt = Kd[r][j] + Kd[r][j] = U1[(tt >> 24) & 0xFF] ^ \ + U2[(tt >> 16) & 0xFF] ^ \ + U3[(tt >> 8) & 0xFF] ^ \ + U4[ tt & 0xFF] + self.Ke = Ke + self.Kd = Kd + + def encrypt(self, plaintext): + if len(plaintext) != self.block_size: + raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(plaintext))) + Ke = self.Ke + + BC = self.block_size / 4 + ROUNDS = len(Ke) - 1 + if BC == 4: + SC = 0 + elif BC == 6: + SC = 1 + else: + SC = 2 + s1 = shifts[SC][1][0] + s2 = shifts[SC][2][0] + s3 = shifts[SC][3][0] + a = [0] * BC + # temporary work array + t = [] + # plaintext to ints + key + for i in xrange(BC): + t.append((ord(plaintext[i * 4 ]) << 24 | + ord(plaintext[i * 4 + 1]) << 16 | + ord(plaintext[i * 4 + 2]) << 8 | + ord(plaintext[i * 4 + 3]) ) ^ Ke[0][i]) + # apply round transforms + for r in xrange(1, ROUNDS): + for i in xrange(BC): + a[i] = (T1[(t[ i ] >> 24) & 0xFF] ^ + T2[(t[(i + s1) % BC] >> 16) & 0xFF] ^ + T3[(t[(i + s2) % BC] >> 8) & 0xFF] ^ + T4[ t[(i + s3) % BC] & 0xFF] ) ^ Ke[r][i] + t = copy.copy(a) + # last round is special + result = [] + for i in xrange(BC): + tt = Ke[ROUNDS][i] + result.append((S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((S[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((S[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((S[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF) + return string.join(map(chr, result), '') + + def decrypt(self, ciphertext): + if len(ciphertext) != self.block_size: + raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(ciphertext))) + Kd = self.Kd + + BC = self.block_size / 4 + ROUNDS = len(Kd) - 1 + if BC == 4: + SC = 0 + elif BC == 6: + SC = 1 + else: + SC = 2 + s1 = shifts[SC][1][1] + s2 = shifts[SC][2][1] + s3 = shifts[SC][3][1] + a = [0] * BC + # temporary work array + t = [0] * BC + # ciphertext to ints + key + for i in xrange(BC): + t[i] = (ord(ciphertext[i * 4 ]) << 24 | + ord(ciphertext[i * 4 + 1]) << 16 | + ord(ciphertext[i * 4 + 2]) << 8 | + ord(ciphertext[i * 4 + 3]) ) ^ Kd[0][i] + # apply round transforms + for r in xrange(1, ROUNDS): + for i in xrange(BC): + a[i] = (T5[(t[ i ] >> 24) & 0xFF] ^ + T6[(t[(i + s1) % BC] >> 16) & 0xFF] ^ + T7[(t[(i + s2) % BC] >> 8) & 0xFF] ^ + T8[ t[(i + s3) % BC] & 0xFF] ) ^ Kd[r][i] + t = copy.copy(a) + # last round is special + result = [] + for i in xrange(BC): + tt = Kd[ROUNDS][i] + result.append((Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((Si[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((Si[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((Si[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF) + return string.join(map(chr, result), '') + +def encrypt(key, block): + return rijndael(key, len(block)).encrypt(block) + +def decrypt(key, block): + return rijndael(key, len(block)).decrypt(block) + +def test(): + def t(kl, bl): + b = 'b' * bl + r = rijndael('a' * kl, bl) + assert r.decrypt(r.encrypt(b)) == b + t(16, 16) + t(16, 24) + t(16, 32) + t(24, 16) + t(24, 24) + t(24, 32) + t(32, 16) + t(32, 24) + t(32, 32)