import string import copy # Modified for CSE 365 by reducing the number of rounds # Create a plaintext.bin and key.bin to test... # dd if=/dev/urandom of=key.bin bs=16 count=1 # dd if=/dev/urandom of-plaintext.bin bs=1024 count=1024 # python2 rijndael-fewerrounds.py # md5sum * ############################################################################# # Original code ported from the Java reference code by Bram Cohen, April 2001, # with the following statement: # # 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 # class Rijndael(object): """ A pure python (slow) implementation of rijndael with a decent interface. 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 """ @classmethod def create(cls): if hasattr(cls, "RIJNDAEL_CREATED"): return # [keysize][block_size] #cls.num_rounds = {16: {16: 10, 24: 12, 32: 14}, 24: {16: 12, 24: 12, 32: 14}, 32: {16: 14, 24: 14, 32: 14}} # Modified for CSE 365 to reduce rounds to 3 cls.num_rounds = {16: {16: 3, 24: 3, 32: 3}, 24: {16: 3, 24: 3, 32: 3}, 32: {16: 3, 24: 3, 32: 3}} cls.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]]] 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] # cls.S-boxes and inverse cls.S-boxes cls.S = [0] * 256 cls.Si = [0] * 256 for i in xrange(256): cls.S[i] = cox[i][0] << 7 for t in xrange(1, 8): cls.S[i] ^= cox[i][t] << (7-t) cls.Si[cls.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 cls.T1 = [] cls.T2 = [] cls.T3 = [] cls.T4 = [] cls.T5 = [] cls.T6 = [] cls.T7 = [] cls.T8 = [] cls.U1 = [] cls.U2 = [] cls.U3 = [] cls.U4 = [] for t in xrange(256): s = cls.S[t] cls.T1.append(mul4(s, G[0])) cls.T2.append(mul4(s, G[1])) cls.T3.append(mul4(s, G[2])) cls.T4.append(mul4(s, G[3])) s = cls.Si[t] cls.T5.append(mul4(s, iG[0])) cls.T6.append(mul4(s, iG[1])) cls.T7.append(mul4(s, iG[2])) cls.T8.append(mul4(s, iG[3])) cls.U1.append(mul4(t, iG[0])) cls.U2.append(mul4(t, iG[1])) cls.U3.append(mul4(t, iG[2])) cls.U4.append(mul4(t, iG[3])) # round constants cls.rcon = [1] r = 1 for t in xrange(1, 30): r = mul(2, r) cls.rcon.append(r) cls.RIJNDAEL_CREATED = True def __init__(self, key, block_size = 16): # create common meta-instance infrastructure self.create() 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 = Rijndael.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] ^= (Rijndael.S[(tt >> 16) & 0xFF] & 0xFF) << 24 ^ \ (Rijndael.S[(tt >> 8) & 0xFF] & 0xFF) << 16 ^ \ (Rijndael.S[ tt & 0xFF] & 0xFF) << 8 ^ \ (Rijndael.S[(tt >> 24) & 0xFF] & 0xFF) ^ \ (Rijndael.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] ^= (Rijndael.S[ tt & 0xFF] & 0xFF) ^ \ (Rijndael.S[(tt >> 8) & 0xFF] & 0xFF) << 8 ^ \ (Rijndael.S[(tt >> 16) & 0xFF] & 0xFF) << 16 ^ \ (Rijndael.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] = Rijndael.U1[(tt >> 24) & 0xFF] ^ \ Rijndael.U2[(tt >> 16) & 0xFF] ^ \ Rijndael.U3[(tt >> 8) & 0xFF] ^ \ Rijndael.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: Rijndael.SC = 0 elif BC == 6: Rijndael.SC = 1 else: Rijndael.SC = 2 s1 = Rijndael.shifts[Rijndael.SC][1][0] s2 = Rijndael.shifts[Rijndael.SC][2][0] s3 = Rijndael.shifts[Rijndael.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] = (Rijndael.T1[(t[ i ] >> 24) & 0xFF] ^ Rijndael.T2[(t[(i + s1) % BC] >> 16) & 0xFF] ^ Rijndael.T3[(t[(i + s2) % BC] >> 8) & 0xFF] ^ Rijndael.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((Rijndael.S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) result.append((Rijndael.S[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) result.append((Rijndael.S[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) result.append((Rijndael.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: Rijndael.SC = 0 elif BC == 6: Rijndael.SC = 1 else: Rijndael.SC = 2 s1 = Rijndael.shifts[Rijndael.SC][1][1] s2 = Rijndael.shifts[Rijndael.SC][2][1] s3 = Rijndael.shifts[Rijndael.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] = (Rijndael.T5[(t[ i ] >> 24) & 0xFF] ^ Rijndael.T6[(t[(i + s1) % BC] >> 16) & 0xFF] ^ Rijndael.T7[(t[(i + s2) % BC] >> 8) & 0xFF] ^ Rijndael.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((Rijndael.Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) result.append((Rijndael.Si[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) result.append((Rijndael.Si[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) result.append((Rijndael.Si[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF) return string.join(map(chr, result), '') # @staticmethod # def encrypt_block(key, block): # return Rijndael(key, len(block)).encrypt(block) # @staticmethod # def decrypt_block(key, block): # return Rijndael(key, len(block)).decrypt(block) @staticmethod def test(): def t(kl, bl): b = 'b' * bl r = Rijndael('a' * kl, bl) x = r.encrypt(b) assert x != b assert r.decrypt(x) == 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) # Rijndael ############################################################################# key = open('key.bin', 'rb').read() plaintext = open('plaintext.bin', 'rb').read() while len(plaintext) % 16 != 0: plaintext = plaintext + b'\0' r = Rijndael(key, block_size = 16) ciphertext = b'' for i in range(0, len(plaintext), 16): c = bytes(r.encrypt(plaintext[i:i+16])) ciphertext = ciphertext + c f = open('ciphertext.bin', 'wb') f.write(bytes(ciphertext)) f.close() cipertext = open('ciphertext.bin', 'rb').read() while len(ciphertext) % 16 != 0: ciphertext = ciphertext + b'\0' plaintext = b'' for i in range(0, len(ciphertext), 16): p = r.decrypt(ciphertext[i:i+16]) plaintext = plaintext + p f = open('plaintext-backatyou.bin', 'wb') f.write(bytes(plaintext)) f.close() print('Done.')