Jenga
この問題はAESをベースにした問題で変更点は2点あります。
- ShiftRowsが
hori
という関数に、MixColumnsがvert
という関数に対応しています。(内容の差異は後で) - 更に奇数ラウンドではhoriのみ使われ、逆に偶数ラウンドではvertが使われます。
AESの攻撃って
基本的にいろいろありますが、代表的なものとして、線形解読法(linear cryptanalysis)や差分解読法(different cryptanalysis)、SboxをAffineとしてlinearととらえるものがありますよね。
この問題では、AESで解析でよく用いられているsquare attackというものを用いて攻撃を行います。
注意としてこの問題では3×3の行列やアルゴリズムが異なるものを扱っている都合、AESとは一部攻撃方法が異なる部分があります。
各関数について
hori
AESで言うとShiftRowsにあたる部分で、数式では$HORI$として、逆関数を$HORI\_INV$としておきます。
horiは水平に3回行いますが簡略化のため1行のみ記載しています。
\(HORI(x) = (4*x_0\oplus 2*x_1\oplus x_2,x_0\oplus 4*x_1\oplus 2*x_2,2*x_0\oplus x_1\oplus4*x_2)\)
さらに、\
\(\begin{eqnarray*}HORI(x\oplus y)&= (4*(x_0\oplus y_0)\oplus2*(x_1\oplus y_1)\oplus(x_2\oplus y_2),\\ &(x_0\oplus y_0)\oplus4*(x_1\oplus y_1)\oplus2*(x_2\oplus y_2),\\ &2*(x_0\oplus y_0)\oplus(x_1\oplus y_1)\oplus4*(x_2\oplus y_2) \\ &=((4*x_0\oplus2*x_1\oplus x_2)\oplus (4*y_0\oplus2*y_1\oplus y_2),\\ &\ \ \ \ (x_0\oplus4*x_1\oplus2*x_2) \oplus (y_0\oplus4*y_1\oplus2*y_2),\\ &\ \ \ \ (2*x_0\oplus x_1\oplus4*x_2)\oplus(2*y_0\oplus y_1\oplus4*y_2))\\ &=HORI(x)\oplus HORI(y)\end{eqnarray*}\)
という分配則も成り立ちます。
vert
AESで言うとMixColumnsにあたる部分で、数式では$VERT$とし、逆関数を$VERT\_INV$としておきます。
今回の関数において、vertは垂直に3回行いますが計算内容はhoriと同じため省略します。また分配則も成立します。
xor
AESで言うとAddRoundKeyにあたる部分で、数式では$i$ roundのxorを$XOR_i$とし、逆関数を$XOR_i\_INV$としておきます。
sbox
AESで言うとSubBytesにあたる部分で、数式では$sbox$、逆関数を$sbox\_INV$とします。
Square attack
1byteだけ変えた入力を繰り返し行い出力の関係性を見ることで鍵を求めてみようというものです。
round 1
1roundだけ見ていきます。入力として、緑の部分だけ変えて、後は固定の値を与えるとします。
具体的には[bytes([i])+b"\x00"*8 for i in range(256)]
みたいな感じです。
結果として、下の図のように変化させた部分が緑の部分へ影響を及ぼしていることがわかります。ただ、このうち6つ分の値は影響を受けず固定なため白にしています。
ここでわかっている部分や計算可能な部分を青で塗りつぶします。
hori
とsbox
は逆関数が存在することで結果的には全ての状態の値がわかります。ということはxorの値もわかるので1roundは簡単に鍵が求まるということになります。
式で表せばこんな感じになります。
\[SBOX(XOR_1(HORI(input)))=output \Leftrightarrow XOR_1(HORI(input)) = SBOX\\_INV(output)\]Round 2
次に2roundを見ていきます。入力は1roundと同様に緑の部分だけ変えて、後は固定の値を与えた結果、以下のようなものを得られます。
同様にわかっている部分を青で埋めていきます。
というわけで、1roundと比較してすべての状態がわかるわけではないとことになっています。
ただ、緑色の部分がわかると仮定し、その時青色で表現します。この場合はどうだろう。
結果的に、1roundと同じよう計算していけば2Rのカギもわかりそうですね。
ただ問題は仮定の部分で、
- 青の部分の値の固定をどうするのか…
こんな時は2つの入力を使って解決します。便宜上、入力を$input_1,input_2$とし、出力を$output_1,output_2$とします。
この時すべての状態で$input_1,input_2$のXORと、$output_1,output_2$のXORを取ります。
すると、2R後のxorが互いに打ち消すので1RのSBOX後まで計算が可能になり、青の部分が鍵の影響を受けずに計算可能になります。更に、1Rのhoriの後の状態を$input’$、1RのSBOX後の状態を$output’$とします。
1つの場所に注目し式で表すと$SBOX(input_1’ \oplus KEY_1) \oplus SBOX(input_2’ \oplus KEY_1) = output_1’ \oplus output_2’$となりそれぞれのbytesごろに探索すれば鍵が求まります。
Round 3
影響が2回被る部分を赤で示しています。ここまでくるとちょっときついですね…
少し、工夫をすると1round分削減できます。
具体的には今まで入力として与えていたものはinput=\x01+\x00*8
みたいなものでした。これに$HORI\_INV(input)$としたものを与えると1Rのhori
にて1つの値にのみ影響を与えることができます。これにより1Rのsbox
直後の値が1つのみにとどまるので2roundと同じ攻撃が起こなうことができます。
Round 4
今回はRound 3で行ったテクニックは用いず考察していきます。Round 2では白の部分を1つの入力用いて固定していました。これを拡張する方向性で考えていきます。具体的には2Rのsbox
直後まではinputの緑によって制御されるということ。それ以降は緑が重複するため制御は厳しそうです。なので、これらをうまく制御して計算途中はわからないが計算結果をわかればよいという方向性で2Rのsbox
直後まで求めていきます。
具体的には以下の式を考えます。 \(\oplus_{0\leq i \leq 255} SBOX(XOR_2(VERT(SBOX(XOR_1(HORI(input_i))))))\)
とはいっても長いので1つずつ見ていきます。
-
1Rのinputの時の緑(左上のみ)
$\oplus_{0\leq i \leq 255} input_i = 0$になります
-
1Rのhori直後の緑(上段のみで左からインデックスを$0\leq j \leq 2$とします) 分配則と1.より$\oplus_{0\leq i \leq 255} HORI(input) = 0$になります。
-
2.と1RのKEYを偶数回XORすることになるので$\oplus_{0\leq i \leq 255} XOR_1(input) = 0$になります。
-
3.において緑の場所に出現している値は256個あるため、$\oplus_{0\leq i \leq 255} SBOX(input) = 0$になります。
同様の議論を省略しますが、2Rの結果が0になるということと分配則を用いることで3Rのxor
直後まで行うことができ、またそれまですべての入力に対してxorを取った値は0になる部分を青で示します。
逆に、output側から逆算できる部分を水色で示すと、以下になります。
式として以下の形になります。
\[\oplus_{0\leq i \leq 255} SBOX\_INV(VERT\_INV(XOR_4\_INV(SBOX\_INV(output_i)))) = 0\]一度、$SBOX\_INV(VERT\_INV(XOR_4\_INV(SBOX\_INV(output_i))))$に関して考えてみます。
$SBOX\_INV(output_i))$は確実に値がわかるので$SBOX\_INV(output_i))=output’$としておきます。
\[\begin{eqnarray*} SBOX\_INV(VERT\_INV(XOR_4\_INV(output'))) &=SBOX\_INV(VERT\_INV(output' \oplus KEY_4))\\ &= SBOX\_INV(VERT\_INV(output') \oplus VERT\_INV(KEY_4)) \end{eqnarray*}\]$VERT\_INV(output’)$は計算可能だが、$VERT\_INV(KEY_4)$は計算できないためこれの計算結果を$KEY_4’$と置くと
$\oplus_{0\leq i \leq 255} SBOX\_INV(VERT\_INV(output’) \oplus KEY_4’)$となり$SBOX\_INV$はそれぞれの値に対して演算を行うことを考慮すれば、$KEY_4’$のそれぞれの値をブルートフォースで求めればよいことになります。その求めた結果から$KEY_4$を復元することができますが、いくつかの候補があるためどの鍵があっているのか最終的にテストする必要があります。
Round 5
round5は以下のようになるがround2,4のテクニックを組み合わせることで解決できます。
このようになり、解決可能へ…
やったね
最終的なスクリプトは以下のようになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from Jenga import *
from ptrlib import *
import re
from itertools import product
import functools
def hori_inv_ret(b):
for i in range(0, 9, 3):
x, y, z = b[i:i+3]
b[i:i+3] = (
gf_mul(x, 0x9e) ^ gf_mul(y, 0x4f),
gf_mul(y, 0x9e) ^ gf_mul(z, 0x4f),
gf_mul(z, 0x9e) ^ gf_mul(x, 0x4f),
)
return b
def vert_inv_ret(b):
for i in range(3):
x, y, z = b[i], b[i + 3], b[i + 6]
b[i], b[i + 3], b[i + 6] = (
gf_mul(x, 0x9e) ^ gf_mul(y, 0x4f),
gf_mul(y, 0x9e) ^ gf_mul(z, 0x4f),
gf_mul(z, 0x9e) ^ gf_mul(x, 0x4f),
)
return b
def hori(b):
for i in range(0, 9, 3):
x, y, z = b[i:i+3]
b[i:i+3] = (
gf_mul(x, 4) ^ gf_mul(y, 2) ^ z,
gf_mul(y, 4) ^ gf_mul(z, 2) ^ x,
gf_mul(z, 4) ^ gf_mul(x, 2) ^ y,
)
return b
def sbox_inv(b):
for i in range(9):
b[i] = SBOX_inv[b[i]]
return b
def recover_key(last_key):
last_key = last_key[::-1]
for i in range(36):
last_key.append(SBOX_inv[last_key[i]]^last_key[i+1])
return last_key[::-1][:9]
# prepare
pts = [bytes(hori_inv_ret(list(bytes([i])+b"\x00"*8))).hex() for i in range(256)]
regex = re.compile("[0-9a-f]{18}")
last_key = [[] for i in range(9)]
io = remote("nc jenga.chal.2024.ctf.acsc.asia 39425")
# io = process(["python3","task.py"])
io.sendline("\n".join(pts))
print("SENDED")
ret = regex.findall(io.recvuntil(b"pt? ").decode())
TEST = ret[0]
AIM = ret[-1]
known_part = [hori_inv_ret(sbox_inv(vert_inv_ret(list(bytes.fromhex(ct))))) for ct in ret[:-1]]
print("RECVED")
last_key = []
for index in range(9):
ret = []
for i in range(256):
if functools.reduce(lambda x, y: x ^ y, [SBOX_inv[ct[index]^i] for ct in known_part])==0:
ret.append(i)
last_key.append(ret)
# recover_last key
for cand in product(*last_key):
test_key = recover_key(hori(list(cand)))
if Jenga(test_key).encrypt(b"\x00"*9).hex() == TEST:
a = Jenga(test_key).decrypt(bytes.fromhex(AIM)).hex()
print(a)
io.sendline(a)
io.sh()
print("found")
break
# ACSC{b40a78c51c581b7478e910df9ede1f50c036eb60a1fcd9b4146c5f92c6fdd348}
最後に
この問題を作成したRBTree (@RBTree_)さんと、攻撃概要を書いていただいたrkm0959 (@rkm0959)さんに感謝します。大変参考になりました。
Appendix
Round 1
1
工事中
Round 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
from Jenga import *
from ptrlib import *
from itertools import product
from kanonlib.Util import bxor
def hori_inv_ret(b):
for i in range(0, 9, 3):
x, y, z = b[i:i+3]
b[i:i+3] = (
gf_mul(x, 0x9e) ^ gf_mul(y, 0x4f),
gf_mul(y, 0x9e) ^ gf_mul(z, 0x4f),
gf_mul(z, 0x9e) ^ gf_mul(x, 0x4f),
)
return b
def vert_inv_ret(b):
for i in range(3):
x, y, z = b[i], b[i + 3], b[i + 6]
b[i], b[i + 3], b[i + 6] = (
gf_mul(x, 0x9e) ^ gf_mul(y, 0x4f),
gf_mul(y, 0x9e) ^ gf_mul(z, 0x4f),
gf_mul(z, 0x9e) ^ gf_mul(x, 0x4f),
)
return b
def hori(b):
for i in range(0, 9, 3):
x, y, z = b[i:i+3]
b[i:i+3] = (
gf_mul(x, 4) ^ gf_mul(y, 2) ^ z,
gf_mul(y, 4) ^ gf_mul(z, 2) ^ x,
gf_mul(z, 4) ^ gf_mul(x, 2) ^ y,
)
return b
def vert(b):
for i in range(3):
x, y, z = b[i], b[i + 3], b[i + 6]
b[i], b[i + 3], b[i + 6] = (
gf_mul(x, 4) ^ gf_mul(y, 2) ^ z,
gf_mul(y, 4) ^ gf_mul(z, 2) ^ x,
gf_mul(z, 4) ^ gf_mul(x, 2) ^ y,
)
return b
def sbox_inv(b):
for i in range(9):
b[i] = SBOX_inv[b[i]]
return b
def recover_key(last_key):
last_key = last_key[::-1]
for i in range(9):
last_key.append(SBOX_inv[last_key[i]]^last_key[i+1])
return last_key[::-1]
key = os.urandom(9)
jenga = Jenga(key)
pt = os.urandom(9)
pt2 = os.urandom(9)
ct1_base = jenga.encrypt_test(pt,2)
ct1 = sbox_inv(hori_inv_ret(list(ct1_base)))
ct2 = jenga.encrypt_test(pt2,2)
ct2 = sbox_inv(hori_inv_ret(list(ct2)))
pt1 = hori(list(pt))
pt2 = hori(list(pt2))
XCT = vert_inv_ret(list(bxor(ct2,ct1)))
cands = []
for i in range(9):
cand = []
for k in range(256):
if SBOX[pt1[i]^k]^SBOX[pt2[i]^k] == XCT[i]:
cand.append(k)
cands.append(cand)
for cand_key in product(*cands):
# exit()
if Jenga(cand_key).encrypt_test(pt,2) == ct1_base:
print(cand_key)
if list(cand_key) == list(key):
print("KK")
exit()
Round 3
1
工事中
Round 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
from Jenga import *
from ptrlib import *
import re
from itertools import product
import functools
def hori_inv_ret(b):
for i in range(0, 9, 3):
x, y, z = b[i:i+3]
b[i:i+3] = (
gf_mul(x, 0x9e) ^ gf_mul(y, 0x4f),
gf_mul(y, 0x9e) ^ gf_mul(z, 0x4f),
gf_mul(z, 0x9e) ^ gf_mul(x, 0x4f),
)
return b
def vert_inv_ret(b):
for i in range(3):
x, y, z = b[i], b[i + 3], b[i + 6]
b[i], b[i + 3], b[i + 6] = (
gf_mul(x, 0x9e) ^ gf_mul(y, 0x4f),
gf_mul(y, 0x9e) ^ gf_mul(z, 0x4f),
gf_mul(z, 0x9e) ^ gf_mul(x, 0x4f),
)
return b
def hori(b):
for i in range(0, 9, 3):
x, y, z = b[i:i+3]
b[i:i+3] = (
gf_mul(x, 4) ^ gf_mul(y, 2) ^ z,
gf_mul(y, 4) ^ gf_mul(z, 2) ^ x,
gf_mul(z, 4) ^ gf_mul(x, 2) ^ y,
)
return b
def vert(b):
for i in range(3):
x, y, z = b[i], b[i + 3], b[i + 6]
b[i], b[i + 3], b[i + 6] = (
gf_mul(x, 4) ^ gf_mul(y, 2) ^ z,
gf_mul(y, 4) ^ gf_mul(z, 2) ^ x,
gf_mul(z, 4) ^ gf_mul(x, 2) ^ y,
)
return b
def sbox_inv(b):
for i in range(9):
b[i] = SBOX_inv[b[i]]
return b
def recover_key(last_key):
last_key = last_key[::-1]
for i in range(27):
last_key.append(SBOX_inv[last_key[i]]^last_key[i+1])
return last_key[::-1][:9]
ROUND = 4
KEY = os.urandom(9)
jenga = Jenga(KEY)
AIM_pt = os.urandom(9)
AIM_ct = jenga.encrypt_test(AIM_pt,ROUND)
# prepare
pts = [bytes([i])+b"\x00"*8 for i in range(256)]
cts = [jenga.encrypt_test(pt,ROUND) for pt in pts]
TEST = cts[0]
known_part = [vert_inv_ret(sbox_inv(hori_inv_ret(list(ct)))) for ct in cts]
last_key = []
for index in range(9):
ret = []
for i in range(256):
if functools.reduce(lambda x, y: x ^ y, [SBOX_inv[ct[index]^i] for ct in known_part])==0:
ret.append(i)
last_key.append(ret)
print(last_key)
# recover_last key
for cand in product(*last_key):
test_key = recover_key(vert(list(cand)))
if Jenga(test_key).encrypt_test(b"\x00"*9,ROUND) == TEST:
print(list(test_key) == list(KEY))
print("found")
break
Round 5
jenga_test.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
SBOX = [
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
]
SBOX_inv = [
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D
]
def gf_mul(a, b):
v = 0
for i in range(8):
v <<= 1
if v & 0x100:
v ^= 0x11b
if (b >> (7 - i)) & 1:
v ^= a
return v
ROUND = 5
class Jenga:
def __init__(self, key: bytes):
assert len(key) == 9
subkeys = list(key)
for i in range( 9 * (ROUND - 1) ):
subkeys.append(SBOX[subkeys[-9] ^ subkeys[-1]])
self.subkeys = [ subkeys[i:i+9] for i in range(0, 9 * ROUND, 9) ]
@staticmethod
def hori(b):
for i in range(0, 9, 3):
x, y, z = b[i:i+3]
b[i:i+3] = (
gf_mul(x, 4) ^ gf_mul(y, 2) ^ z,
gf_mul(y, 4) ^ gf_mul(z, 2) ^ x,
gf_mul(z, 4) ^ gf_mul(x, 2) ^ y,
)
@staticmethod
def vert(b):
for i in range(3):
x, y, z = b[i], b[i + 3], b[i + 6]
b[i], b[i + 3], b[i + 6] = (
gf_mul(x, 4) ^ gf_mul(y, 2) ^ z,
gf_mul(y, 4) ^ gf_mul(z, 2) ^ x,
gf_mul(z, 4) ^ gf_mul(x, 2) ^ y,
)
@staticmethod
def hori_inv(b):
for i in range(0, 9, 3):
x, y, z = b[i:i+3]
b[i:i+3] = (
gf_mul(x, 0x9e) ^ gf_mul(y, 0x4f),
gf_mul(y, 0x9e) ^ gf_mul(z, 0x4f),
gf_mul(z, 0x9e) ^ gf_mul(x, 0x4f),
)
@staticmethod
def vert_inv(b):
for i in range(3):
x, y, z = b[i], b[i + 3], b[i + 6]
b[i], b[i + 3], b[i + 6] = (
gf_mul(x, 0x9e) ^ gf_mul(y, 0x4f),
gf_mul(y, 0x9e) ^ gf_mul(z, 0x4f),
gf_mul(z, 0x9e) ^ gf_mul(x, 0x4f),
)
def xor(self, b, rnd):
for i in range(9):
b[i] ^= self.subkeys[rnd][i]
@staticmethod
def sbox(b):
for i in range(9):
b[i] = SBOX[b[i]]
@staticmethod
def sbox_inv(b):
for i in range(9):
b[i] = SBOX_inv[b[i]]
def encrypt(self, block: bytes):
b = list(block)
for rnd in range(ROUND + 1):
self.vert(b) if rnd % 2 else self.hori(b)
if rnd == ROUND:
break
self.xor(b, rnd)
self.sbox(b)
break
return bytes(b)
def encrypt_test(self, block: bytes, ROUND: int):
b = list(block)
for rnd in range(ROUND + 1):
self.vert(b) if rnd % 2 else self.hori(b)
if rnd == ROUND:
break
self.xor(b, rnd)
self.sbox(b)
return bytes(b)
def decrypt(self, block: bytes):
b = list(block)
for rnd in reversed(range(ROUND + 1)):
self.vert_inv(b) if rnd % 2 else self.hori_inv(b)
if rnd == 0:
break
self.sbox_inv(b)
self.xor(b, rnd - 1)
return bytes(b)
if __name__ == "__main__":
import os
for _ in range(10):
key = os.urandom(9)
cipher = Jenga(key)
for _ in range(100):
pt = os.urandom(9)
assert cipher.decrypt(cipher.encrypt(pt)) == pt
assert cipher.encrypt(cipher.decrypt(pt)) == pt