They say 3 rounds is provably secure, right? Download

How to solve

Permutations can be handled as matrices.

For given key $k$ and plaintext $(x, y)$, the result of one-round encryption is:

$$(x_1, y_1) = (y, xk^{-1}yk)$$

Let’s do this for three rounds!

$$(x_2, y_2) = (xk^{-1}yk, yk^{-1}xk^{-1}yk^2)$$ $$(x_3, y_3) = (y_2, xk^{-1}ykk^{-1}y_2k) = (y_2, xk^{-1}yy_2k)$$

If we define $t = yy_2$, we can say that

$$(yx_3, y_3) = (t, xk^{-1}tk)$$

So, the three-round encryption is not different from the one-round one!

From this, just run backtracking for $x^{-1}y_3 = k^{-1}tk$ to get proper $k$ that satisfies the both sets.

import random
from hashlib import sha1
from horst import Permutation

with open('data.txt', 'r') as f:
    data = eval(f.read())

(x1, y1), (t1, u1) = data[0]
(x2, y2), (t2, u2) = data[1]

# Left-hand side: l
l1, l2 = x1.inv() * u1, x2.inv() * u2
# Right-hand side: k^{-1} r k
r1, r2 = y1 * t1, y2 * t2

k = [-1 for _ in range(64)]
kinv = [-1 for _ in range(64)]
called = []

def backtrack(idx):
    if idx in called:
        st = set(range(64)) - set(called)
        if len(st) == 0:
            k_str = str(Permutation(k)).encode('ascii')
            print("PCTF{%s}" % sha1(k_str).hexdigest())
            exit(0)
        backtrack(min(st))
        return

    called.append(idx)
    p, q = l1[idx], l2[idx]

    # Running backtracking for: `l = k^{-1} r k`
    # If `l[idx] = p`, then `k[r[kinv[idx]]] = p`
    # Let's say `kinv[idx] = a, r[a] = b, k[b] = p`.
    # Just do possible all the (a, b) pairs for backtracking.

    # Possible `a` values
    if kinv[idx] != -1:
        arange = [kinv[idx]]
    else:
        # Values which are not used
        arange = list(set(range(64)) - set(kinv))

    for a in arange:

        # If k[a] is assigned, flag_ka is True.
        flag_ka = False
        if kinv[idx] == -1:
            flag_ka = True
            kinv[idx] = a
            k[a] = idx

        b = r1[a]
        c = r2[a]
        
        if ((k[b] != -1 and k[b] != p) or (kinv[p] != -1 and kinv[p] != b)
           or (k[c] != -1 and k[c] != q) or (kinv[q] != -1 and kinv[q] != c)):
            # Set back
            if flag_ka:
                kinv[idx] = -1
                k[a] = -1
            continue

        # If k[b] is assigned, flag_kb is True, and so on.
        flag_kb, flag_kc = False, False
        if k[b] == -1:
            flag_kb = True
            k[b] = p
            kinv[p] = b
        if k[c] == -1:
            flag_kc = True
            k[c] = q
            kinv[q] = c
            
        backtrack(p)
        
        # Set back
        if flag_kc:
            k[c] = -1
            kinv[q] = -1
        if flag_kb:
            k[b] = -1
            kinv[p] = -1
        if flag_ka:
            kinv[idx] = -1
            k[a] = -1

    called.pop()

backtrack(0)

The flag is PCTF{69f4153d282560cdaab05e14c9f1b7e0a5cc74d1}.