This post is about Rijndael S-box which the first stage of AES encryption. It’s substitution lookup table. AES implementation usually uses pre-computed table. I thought it would be fun to calculate it in python to see it action.
From Wiki
the input is mapped to its multiplicative inverse in GF(28) = GF(2) [x]/(x8 + x4 + x3 + x + 1), Rijndael’s finite field. Zero, as the identity, is mapped to itself. This transformation is known as the Nyberg S-box after its inventor Kaisa Nyberg.[2] The multiplicative inverse is then transformed using the following affine transformation:
There are 2 stages
- Multiplicative inverse in GF(2^8)
- Affine transformation
Multiplicative inverse Link to heading
I am using brute-force in GF(256) to find inverse. Basically loop and break when GF multiplication results in 1.
p . q mod 0x11b = 1
Based on Link, The calculation
def mult(a,b):
product = 0
for i in range(8):
if (b & 1) == 1:
product ^= a
hi_bit_set = a & 0x80
a = (a << 1) & 0xFF
if hi_bit_set == 0x80:
a ^= 0x1B
b >>= 1
return product
def get_inverse(n):
if n == 0 :
return 0
e = None
for i in range(256):
m = mult(n, i)
if (m == 1):
e = i
break
return e
Affinity transformation Link to heading
For the Affinity transformation, It can be calculated with multiple algorithms But I implemented the formula:
S = B + (B « 1) + (B « 2) + (B « 3) + (B « 4) + 0x63
Where +
is bitwise xor and <<
is left rotate.
B here is the output is multiplicative inverse above
For rotation, I used good old concat after transforming it with int2binstr
. There is probably easier pythonic way but It’s 2 AM on Saturday night.
def rot1(v):
bb = int2binstr(v)
br = bb[1:] + bb[0]
br = int(br, 2)
return br
Putting it all together Link to heading
def int2binstr(i):
x = (bin(i)[2:]).zfill(8)
return x
def rot1(v):
bb = int2binstr(v)
br = bb[1:] + bb[0]
br = int(br, 2)
return br
def rot(v, count):
p = v
for ii in range(count):
p = rot1(p)
return p
def affinity(b):
s = b ^ rot(b, 1) ^ rot(b, 2) ^ rot(b, 3) ^ rot(b, 4) ^ 0x63
return s
def mult(a,b):
product = 0
for i in range(8):
if (b & 1) == 1:
product ^= a
hi_bit_set = a & 0x80
a = (a << 1) & 0xFF
if hi_bit_set == 0x80:
a ^= 0x1B
b >>= 1
return product
def get_inverse(n):
if n == 0 :
return 0
e = None
for i in range(256):
m = mult(n, i)
if (m == 1):
e = i
break
return e
for n in range(256):
e = get_inverse(n)
s = affinity(e)
print(f'[{int(n/16)}][{n%16}]={hex(s)}')