"""
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 base64

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 range(255):
	j = (alog[-1] << 1) ^ alog[-1]
	if j & 0x100 != 0:
		j ^= 0x11B
	alog.append(j)

log = [0] * 256
for i in range(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 range(256)]
box[1][7] = 1
for i in range(2, 256):
	j = alog[255 - log[i]]
	for t in range(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 range(256)]
for i in range(256):
	for t in range(8):
		cox[i][t] = B[t]
		for j in range(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 range(256):
	S[i] = cox[i][0] << 7
	for t in range(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 range(4)]

for i in range(4):
	for j in range(4):
		AA[i][j] = G[i][j]
		AA[i][i+4] = 1

for i in range(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 range(8):
				AA[i][j], AA[t][j] = AA[t][j], AA[i][j]
			pivot = AA[i][i]
	for j in range(8):
		if AA[i][j] != 0:
			AA[i][j] = alog[(255 + log[AA[i][j] & 0xFF] - log[pivot & 0xFF]) % 255]
	for t in range(4):
		if i != t:
			for j in range(i+1, 8):
				AA[t][j] ^= mul(AA[i][j], AA[t][i])
			AA[t][i] = 0

iG = [[0] * 4 for i in range(4)]

for i in range(4):
	for j in range(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 |= mul(a, b)
	return r

T1 = []
T2 = []
T3 = []
T4 = []
T5 = []
T6 = []
T7 = []
T8 = []
U1 = []
U2 = []
U3 = []
U4 = []

for t in range(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 range(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 range(ROUNDS + 1)]
		# decryption round keys
		Kd = [[0] * BC for i in range(ROUNDS + 1)]
		ROUND_KEY_COUNT = (ROUNDS + 1) * BC
		KC = len(key) // 4

		# copy user material bytes into temporary ints
		tk = []
		for i in range(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 range(1, KC):
					tk[i] ^= tk[i-1]
			else:
				for i in range(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 range(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 range(1, ROUNDS):
			for j in range(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 range(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 range(1, ROUNDS):
			for i in range(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 range(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 ''.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 range(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 range(1, ROUNDS):
			for i in range(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 range(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 ''.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)


class zeropad:
	def __init__(self, block_size):
		assert 0 < block_size < 256
		self.block_size = block_size

	def pad(self, pt):
		ptlen = len(pt)
		padsize = self.block_size - ((ptlen + self.block_size - 1) % self.block_size + 1)
		return pt + "\0" * padsize

	def unpad(self, ppt):
		assert len(ppt) % self.block_size == 0
		offset = len(ppt)
		if offset == 0:
			return ''
		end = offset - self.block_size + 1
		while offset > end:
			offset -= 1
			if ppt[offset] != "\0":
				return ppt[:offset + 1]
		assert False

class cbc:
	def __init__(self, padding, cipher, iv):
		assert padding.block_size == cipher.block_size
		assert len(iv) == cipher.block_size
		self.padding = padding
		self.cipher = cipher
		self.iv = iv

	def encrypt(self, pt):
		ppt = self.padding.pad(pt)
		offset = 0
		ct = ''
		v = self.iv
		while offset < len(ppt):
			block = ppt[offset:offset + self.cipher.block_size]
			block = self.xorblock(block, v)
			block = self.cipher.encrypt(block)
			ct += block
			offset += self.cipher.block_size
			v = block
		return ct

	def decrypt(self, ct):
		assert len(ct) % self.cipher.block_size == 0
		ppt = ''
		offset = 0
		v = self.iv
		while offset < len(ct):
			block = ct[offset:offset + self.cipher.block_size]
			decrypted = self.cipher.decrypt(block)
			ppt += self.xorblock(decrypted, v)
			offset += self.cipher.block_size
			v = block
		pt = self.padding.unpad(ppt)
		return pt

	def xorblock(self, b1, b2):
		# sorry, not very Pythonesk
		i = 0
		r = ''
		while i < self.cipher.block_size:
			r += chr(ord(b1[i]) ^ ord(b2[i]))
			i += 1
		return r



def decryptRinjdael(key, iv, data, areBase64 = False):
	"""
	Where the magic happens

	key -- AES key (string)
	IV -- IV thing (string)
	data -- data to decrypt (string)
	areBase64 -- if True, iv and data are passed in base64
	"""
	if areBase64:
		iv = base64.b64decode(iv).decode("latin_1")
		data = base64.b64decode(data).decode("latin_1")

	r = rijndael(key, 32)
	p = zeropad(32)
	c = cbc(p, r, iv)
	return str(c.decrypt(data))
