NSUCRYPTO 2024

Trong hai bài viết mình sẽ giới thiệu cách giải các bài trong NSUCRYPTO 2024 từ bài giải của team mình lẫn từ team các bạn mà mình tham khảo sau khi kết thúc giải. Hiện tại chỉ có team của Robin Jadoul, Jack Pope, Esrever Yievs chia sẻ bài giải gần hoàn chỉnh trên discord của một cộng đồng toxic nào đó mà ai cũng biết nên mình sẽ gọi là team JPY khi đề cập tới cách giải của các bạn (lấy chữ cái đầu họ của ba người :v).

Lời nói đầu

Đầu tiên mình xin cám ơn rất nhiều người đã ủng hộ, giúp đỡ mình trong mấy năm qua, để mình có thể đi tới ngày hôm nay.

Đề thi năm nay khiến mình có chút hoang mang khi bắt đầu làm. Round 2 có 11 câu với hai câu special prize. Thường những năm trước có từ 4 tới 5 câu special prize và đây là nơi đột biến xảy ra. Nếu chỉ có hai câu thì mình không hy vọng được giải cao mà mục tiêu là giải trọn vẹn và không mắc lỗi ở tất cả câu khác.

Và điều kì diệu đã xảy ra :))) team mình được hai bài BEST SOLUTION do hai bạn teammate giải, còn mình thì ... ké :D

Chân thành cám ơn bạn Chương và bạn Uyên đã đồng hành cũng mình hai năm nay.

RSA signature

Đây là bài 7 ở round 1 và bài 1 ở round 2.

Đề bài

Chúng ta cần ký thông điệp \(M\) bằng thuật toán RSA. Như bình thường, đặt \(N = p \cdot q\) là RSA-modulus với \(p\)\(q\) là hai số nguyên tố lớn.

Đặt \(e\) là public exponent và \(d\) là secret exponent với

\[e \cdot d = 1 \bmod (p - 1)(q - 1).\]

Signature của chúng ta sẽ là

\[S = M^d \bmod N.\]

Giả sử kẻ tấn công biết giá trị

\[M_p = M^{d_p} \bmod p,\]

nhưng không biết giá trị

\[M_q = M^{d_q} \bmod q,\]

trong đó

\[d_p = d \bmod (p-1), \quad d_q = d \bmod (q-1).\]

Nếu kẻ tấn công biết modulo \(N\) (nhưng không biết \(p\)\(q\)), biết public exponent \(e\) (nhưng không biết \(d\)) và biết thông điệp gốc \(M\). Các thông số bí mật nào kẻ tấn công có thể tìm ra?

Lời giải

\(S \equiv M^d \pmod N\) nên suy ra \(S \equiv M^d \pmod p\)

\[(S - M^d) \ \vdots \ N \ \vdots \ p,\]

tương tự \(S \equiv M^d \pmod q\).

Ngoài ra ta có

(9)\[S^e \equiv M^{ed} \equiv M \pmod N.\]

Tương tự phía trên ta cũng có

\[S^e \equiv M^{ed} \equiv M \pmod p,\]

nhưng \(d_p \equiv d \pmod{p-1}\), tương đương với

\[d = d_p + k_p \cdot (p-1)\]

với \(k_p \in \mathbb{Z}\).

Do đó

(10)\[\begin{split}S^e & \equiv M^{e \cdot (d_p + k_p \cdot (p - 1))} \pmod{p} \\ & \equiv M^{e \cdot d_p} \pmod{p} \quad (\text{định lý Euler}) \\ & \equiv M_p^e \pmod{p}.\end{split}\]

Từ hai phương trình (9)(10) suy ra

\[M^e_p \equiv M \pmod p,\]

hay ta có thể nói là

\[(M^e_p \bmod N) - M \ \vdots \ p.\]

Từ đây, sử dụng thuật toán Euclid ta tìm được

\[p = \gcd((M^e_p \bmod N) - M, N).\]

Vậy là ta đã tìm được \(p\) nên có thể tìm được tất cả tham số secret khác của RSA là \(q\), \(d\), \(d_p\)\(d_q\).

AntCipher 2.0

Đây là bài 2 ở round 2.

Đề bài

Một phút một lần, GPS được track bởi vệ tinh. Khi đó, vĩ độ ở dạng IEEE 754 single-precision floating-point sẽ được chuyển thành dãy 32 bit. Tương tự cho kinh độ.

Khi đó hai dãy nhị phân sẽ được nối lại (lattile || longitude) thành plaintext có 64 bit. Sau đó plaintext được XOR với keystream để tạo thành ciphertext 64 bit.

Cách thực hiện sinh khóa như sau. Tại điểm khởi tạo, secret key 64 bit được viết vào thanh ghi \(R\) dung lượng 64 bit.

Với mỗi số \(i\) với \(i \geqslant 1\), dãy \(K_i\) 64 bit sẽ được sinh từ input là \(R\) và keystream cũng dùng để cập nhật thanh ghi \(R\), nghĩa là sau khi tính toán thì giá trị \(K_i\) sẽ được ghi vào \(R\).

Xét dạng CNF của hàm \(C\), với CNF là conjunction of disjunction (tích của các tổng), có dạng sau:

\[\begin{split}C = (x_1 \lor v_2 \lor \neg x_5) \land (\neg x_1 \lor \neg x_2 \lor x_5) \land (x_1 \lor x_3 \lor \neg x_5) \land (\neg x_1 \lor \neg x_3 \lor x_5) \\ \land (x_2 \lor x_3 \lor \neg x_5) \land (\neg x_2 \lor \neg x_3 \lor x_5) \land (x_1 \lor x_2 \lor \neg x_6) \land (\neg x_1 \lor \neg x_2 \lor x_6) \\ \land (x_1 \lor x_4 \lor \neg x_6) \land (\neg x_1 \lor \neg x_4 \lor x_6) \land (x_2 \lor x_4 \lor \neg x_6) \land (\neg x_2 \lor \neg x_4 \lor x_6) \\ \land (x_1 \lor x_3 \lor \neg x_7) \land (\neg x_1 \lor \neg x_3 \lor x_7) \land (x_1 \lor x_4 \lor \neg x_7) \land (\neg x_1 \lor \neg x_4 \lor x_7) \\ \land (x_3 \lor x_4 \lor \neg x_7) \land (\neg x_3 \lor \neg x_4 \lor x_7) \land (x_2 \lor x_3 \lor \neg x_8) \land (\neg x_2 \lor \neg x_3 \lor x_8) \\ \land (x_2 \lor x_4 \lor \neg x_8) \land (\neg x_2 \lor \neg x_4 \lor x_8) \land (x_3 \lor x_4 \lor \neg x_8) \land (\neg x_3 \lor \neg x_4 \lor x_8)\end{split}\]

Phương trình \(C = 1\) biểu diễn một hàm không tuyến tính \(F_G\) lấy đầu vào là \(4\) bits \((x_1, x_2, x_3, x_4)\) và sinh ra \(4\) bits \((x_5, x_6, x_7, x_8)\) làm output.

Ở vòng lặp thứ \(i\) của cipher, \(64\) bits của \(R\) được chia thành \(16\) đoạn, mỗi đoạn \(4\) bit. Mỗi đoạn thành input cho \(F_G\) và kết quả là \(16\) đoạn \(4\) bits mới được nối với nhau thành \(K_i\)\(64\) bits.

\(1704\) ciphertexts được truyền đi không gặp vấn đề. Tuy nhiên ở hai keystream tiếp theo gặp sự cố về đĩa cứng nên chỉ nhận được ciphertext thứ \(1704\) là dãy

1001 1000 0011 1101 0110 0011 1101 0101 1011 0011 1011 0111 0000 0000 1000 0011

và hai keystrem \(1702\)\(1703\) nhưng bị mất một phần:

\[\begin{split}K_{1702} & = \text{0101 1001 1111 0011 00X1 X111 1X00 00X0} \\ & \text{111X X000 XXXX XXXX XXXX XXXX XXXX XXXX}, \\ K_{1703} & = \text{XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX} \\ & \text{X111 000X X010 01X1 0X10 0101 0000 1111}.\end{split}\]

Hãy tìm lại plaintext thứ \(1704\).

Lời giải

Đầu tiên mình cần tìm những bộ số \((x_1, \ldots, x_8)\)\(C = 1\).

from itertools import product

def f(x1, x2, x3, x4, x5, x6, x7, x8):
    result = 1
    result = result & (x1 | x2 | ~x5)
    result = result & (~x1 | ~x2 | x5)
    result = result & (x1 | x3 | ~x5)
    result = result & (~x1 | ~x3 | x5)
    result = result & (x2 | x3 | ~x5)
    result = result & (~x2 | ~x3 | x5)
    result = result & (x1 | x2 | ~x6)
    result = result & (~x1 | ~x2 | x6)
    result = result & (x1 | x4 | ~x6)
    result = result & (~x1 | ~x4 | x6)
    result = result & (x2 | x4 | ~x6)
    result = result & (~x2 | ~x4 | x6)
    result = result & (x1 | x3 | ~x7)
    result = result & (~x1 | ~x3 | x7)
    result = result & (x1 | x4 | ~x7)
    result = result & (~x1 | ~x4 | x7)
    result = result & (x3 | x4 | ~x7)
    result = result & (~x3 | ~x4 | x7)
    result = result & (x2 | x3 | ~x8)
    result = result & (~x2 | ~x3 | x8)
    result = result & (x2 | x4 | ~x8)
    result = result & (~x2 | ~x4 | x8)
    result = result & (x3 | x4 | ~x8)
    result = result & (~x3 | ~x4 | x8)

    return result

key = { }

for x1, x2, x3, x4, x5, x6, x7, x8 in product(range(2), repeat=8):
    t = f(x1, x2, x3, x4, x5, x6, x7, x8)
    if t == 1:
        print(f"{x1, x2, x3, x4} -> {x5, x6, x7, x8}")
        a = x1 + (x2 << 1) + (x3 << 2) + (x4 << 3)
        b = x5 + (x6 << 1) + (x7 << 2) + (x8 << 3)
        key[a] = b

Bảng biến đổi sẽ như sau:

\((x_1, x_2, x_3, x_4)\)

\(\rightarrow\)

\((x_5, x_6, x_7, x_8)\)

\((0, 0, 0, 0)\)

\(\rightarrow\)

\((0, 0, 0, 0)\)

\((0, 0, 0, 1)\)

\(\rightarrow\)

\((0, 0, 0, 0)\)

\((0, 0, 1, 0)\)

\(\rightarrow\)

\((0, 0, 0, 0)\)

\((0, 0, 1, 1)\)

\(\rightarrow\)

\((0, 0, 1, 1)\)

\((0, 1, 0, 0)\)

\(\rightarrow\)

\((0, 0, 0, 0)\)

\((0, 1, 0, 1)\)

\(\rightarrow\)

\((0, 1, 0, 1)\)

\((0, 1, 1, 0)\)

\(\rightarrow\)

\((1, 0, 0, 1)\)

\((0, 1, 1, 1)\)

\(\rightarrow\)

\((1, 1, 1, 1)\)

\((1, 0, 0, 0)\)

\(\rightarrow\)

\((0, 0, 0, 0)\)

\((1, 0, 0, 1)\)

\(\rightarrow\)

\((0, 1, 1, 0)\)

\((1, 0, 1, 0)\)

\(\rightarrow\)

\((1, 0, 1, 0)\)

\((1, 0, 1, 1)\)

\(\rightarrow\)

\((1, 1, 1, 1)\)

\((1, 1, 0, 0)\)

\(\rightarrow\)

\((1, 1, 0, 0)\)

\((1, 1, 0, 1)\)

\(\rightarrow\)

\((1, 1, 1, 1)\)

\((1, 1, 1, 0)\)

\(\rightarrow\)

\((1, 1, 1, 1)\)

\((1, 1, 1, 1)\)

\(\rightarrow\)

\((1, 1, 1, 1)\)

Có thể thấy đầu ra không chứa đủ \(16\) trường hợp mà chỉ gồm các trường hợp là

\[(0, 0, 0, 0), (0, 0, 1, 1), (0, 1, 0, 1), (1, 0, 0, 1), (0, 1, 1, 0,), (1, 0, 1, 0), (1, 1, 0, 0), (1, 1, 1, 1).\]

Ở đây mỗi vector đều có số lượng chẵn phần tử \(1\).

Xét biến đổi từ \(K_{1702}\) thành \(K_{1703}\) như sau:

\(K_{1702}\)

\(\rightarrow\)

\(K_{1703}\)

\(\text{0101}\)

\(\rightarrow\)

\(\text{XXXX}\)

\(\text{1001}\)

\(\rightarrow\)

\(\text{XXXX}\)

\(\text{1111}\)

\(\rightarrow\)

\(\text{XXXX}\)

\(\text{0011}\)

\(\rightarrow\)

\(\text{XXXX}\)

\(\text{00X1}\)

\(\rightarrow\)

\(\text{XXXX}\)

\(\text{X111}\)

\(\rightarrow\)

\(\text{XXXX}\)

\(\text{1X00}\)

\(\rightarrow\)

\(\text{XXXX}\)

\(\text{00X0}\)

\(\rightarrow\)

\(\text{XXXX}\)

\(\text{111X}\)

\(\rightarrow\)

\(\text{X111}\)

\(\text{X000}\)

\(\rightarrow\)

\(\text{000X}\)

\(\text{XXXX}\)

\(\rightarrow\)

\(\text{X010}\)

\(\text{XXXX}\)

\(\rightarrow\)

\(\text{01X1}\)

\(\text{XXXX}\)

\(\rightarrow\)

\(\text{0X10}\)

\(\text{XXXX}\)

\(\rightarrow\)

\(\text{0101}\)

\(\text{XXXX}\)

\(\rightarrow\)

\(\text{0000}\)

\(\text{XXXX}\)

\(\rightarrow\)

\(\text{1111}\)

Dựa vào bảng biến đổi ở trên và lưu ý đầu ra (có \(8\) trường hợp tất cả) thì ta có kết luận cho từng bộ \(4\) như sau:

  1. \(0101 \rightarrow 010\).

  2. \(1001 \rightarrow 0110\).

  3. \(1111 \rightarrow 1111\).

  4. \(0011 \rightarrow 0011\).

  5. \(\text{00X1} \rightarrow 0000\). Vì \(K_{1702}\) nhận được từ \(K_{1701}\) nên X chỉ có thể là \(1\) và kết quả ở \(K_{1703}\) sẽ là \(0000\).

  6. \(\text{X111} \rightarrow 1111\).

  7. \(\text{1X00} \rightarrow 0000\). Tương tự, vì \(K_{1702}\) nhận được từ \(K_{1701}\) nên X chỉ có thể là \(1\) và kết quả ở \(K_{1703}\)\(0000\).

  8. \(\text{0X00} \rightarrow 0000\).

  9. \(\text{111X} \rightarrow 1111\).

  10. \(\text{X000} \rightarrow 0000\).

  11. \(\text{XXXX} \rightarrow 1010\). Trong mỗi bộ số trong \(8\) trường hợp ở trên thì số lượng chữ số \(1\) là chẵn.

  12. \(\text{XXXX} \rightarrow 0101\). Tương tự.

  13. \(\text{XXXX} \rightarrow 0110\). Tương tự

  14. \(\text{XXXX} \rightarrow 0101\).

  15. \(\text{XXXX} \rightarrow 0000\).

  16. \(\text{XXXX} \rightarrow 1111\).

Như vậy mình đã khôi phục được \(K_{1703}\) và có thể decrypt.

# https://gist.github.com/AlexEshoo/d3edc53129ed010b0a5b693b88c7e0b5
def ieee_754_conversion(n, sgn_len=1, exp_len=8, mant_len=23):
    """
    Converts an arbitrary precision Floating Point number.
    Note: Since the calculations made by python inherently use floats, the accuracy is poor at high precision.
    :param n: An unsigned integer of length `sgn_len` + `exp_len` + `mant_len` to be decoded as a float
    :param sgn_len: number of sign bits
    :param exp_len: number of exponent bits
    :param mant_len: number of mantissa bits
    :return: IEEE 754 Floating Point representation of the number `n`
    """
    if n >= 2 ** (sgn_len + exp_len + mant_len):
        raise ValueError("Number n is longer than prescribed parameters allows")

    sign = (n & (2 ** sgn_len - 1) * (2 ** (exp_len + mant_len))) >> (exp_len + mant_len)
    exponent_raw = (n & ((2 ** exp_len - 1) * (2 ** mant_len))) >> mant_len
    mantissa = n & (2 ** mant_len - 1)

    sign_mult = 1
    if sign == 1:
        sign_mult = -1

    if exponent_raw == 2 ** exp_len - 1:  # Could be Inf or NaN
        if mantissa == 2 ** mant_len - 1:
            return float('nan')  # NaN

        return sign_mult * float('inf')  # Inf

    exponent = exponent_raw - (2 ** (exp_len - 1) - 1)

    if exponent_raw == 0:
        mant_mult = 0  # Gradual Underflow
    else:
        mant_mult = 1

    for b in range(mant_len - 1, -1, -1):
        if mantissa & (2 ** b):
            mant_mult += 1 / (2 ** (mant_len - b))

    return sign_mult * (2 ** exponent) * mant_mult

ciphertext = "1001 1000 0011 1101 0110 0011 1101 0101 1011 0011 1011 0111 0000 0000 1000 0011"
ciphertext = list(map(int, "".join(ciphertext.split(" "))))

keystream = "0101 0110 1111 0011 0011 1111 1100 0000 1111 0000 1010 0101 0110 0101 0000 1111"
keystream = [int(i, 2) for i in keystream.split(" ")]
keystream = [key[k] for k in keystream] # Find K_{1704} from K_{1703} by table 1
keystream = "".join(f"{k:04b}" for k in keystream)
keystream = list(map(int, keystream))

plaintext = [c^k for c, k in zip(ciphertext, keystream)]  # Recover plaintext
plaintext = "".join(map(str, plaintext))
assert len(plaintext) == 64
m, n = int(plaintext[:32], 2), int(plaintext[32:], 2)     # Extract first 32 bits and last 32 bits

latitude = ieee_754_conversion(m)   # Convert first 32 bits to latitude
longitude = ieee_754_conversion(n)  # Convert last 32 bits to longitude

print(f"({latitude}, {longitude})")

Mình nhận được tọa độ là

\[(-25.79496192932129, 146.58416748046875).\]

Tọa độ này chỉ tới Australia (Augathella), cụ thể là một công viên tên là "Meat Ant Park". Chữ "Ant" có vẻ khớp với đề bài.

Steganography and codes

Đây là bài 3 ở round 2.

Đề bài

Sam và Betty sử dụng kênh mở cho giao tiếp bí mật. Họ không muốn ai biết về hội thoại của mình.

Sam có thể gửi co Betty một trong \(16\) thông điệp.

Khi đó, Sam lấy một ảnh với format RGB bất kì, đổi pixel đầu tiên theo cách nào đó và publish lên website.

Betty sẽ tải bức ảnh đó xuống và phân tích xem Sam đã gửi thông điệp nào.

Sam sẽ làm gì với bức ảnh? Chúng ta nên chú ý rằng phải thay đổi pixel sao cho không thể nhận biết sự thay đổi bởi mắt thường.

Cụ thể, mỗi pixel RGB được biểu diễn ở dạng \(24\) bit:

  • \(8\) bit đầu là độ sáng của màu đỏ \((r_1, \ldots, r_8)\);

  • \(8\) bit tiếp theo là độ sáng màu xanh lá \((g_1, \ldots, g_8)\);

  • \(8\) bit cuối là độ sáng màu xanh dương \((b_1, \ldots, b_8)\).

Sam không được thay đổi các bit \(r\), \(g\), \(b\) từ \(1\) tới \(5\) vì điều đó sẽ dễ nhận thấy khi nhìn bằng mắt.

Nếu Sam đổi một trong các bit \(r_6\), \(r_7\), \(g_6\), \(g_7\)\(b_6\) sẽ tốn \(2\) coins mỗi lần đổi.

Nếu Sam đổi một trong các bit \(r_8\), \(g_8\), \(b_7\)\(b_8\) thì tốn \(1\) coin mỗi lần đổi.

Hãy đưa ra một phương pháp coding cho \(16\) thông điệp vào một pixel mà tốn không quá \(2\) coins (mà không thể nhận thấy bằng mắt thường).

Thêm nữa hãy đưa ra một phương để Betty có thể biết được thông điệp mà Sam giấu vào ảnh là gì. Lưu ý rằng Betty không biết ảnh gốc như thế nào.

Lời giải

Tạm thời trống.

Weak key schedule for DES

Đây là bài 6 ở round 1 và bài 4 ở round 2.

Đề bài

Alice sử dụng thuật toán DES để mã hóa file Book.txt thành Book_Cipher.txt. Tuy nhiên khi code thuật toán DES thì Alice đã code lỗi và khiến cả thuật toán chỉ sử dụng subkey đầu tiên cho tất cả các vòng (thay vì mỗi vòng mỗi khóa con như DES chuẩn).

Hãy giúp Carol tìm lại khóa ban đầu và decrypt bản mã sau:

86991641D28259604412D6BA88A5C0A6471CA722
2C52482BF2D0E841D4343DFB877DC8E0147F3D5F
20FC18FF28CB5C4DA8A0F4694861AB5E98F37ADB
C2D69B35779D9001BB4B648518FE6EBC00B2AB10

Lời giải

Bài này được giải bởi bạn Chương và được thêm BEST SOLUTION :)))

Ý tưởng của bài này là slide attack. Các bạn có thể xem lời giải ở bài viết về slide attack của mình.

Reverse engineering

Đây là bài 5 ở round 2.

Đề bài

Sau khi reverse engineering cài đặt của một thuật toán mật mã nào đó chưa biết thì Bob nhận được hàm boolean sau:

\[f_{2n} (x_1, \ldots, x_{2n}) = \bigoplus_{i=1}^n x_i x_{i+n} \prod_{j=i+1}^n (x_j \oplus x_{j + n}).\]

Bob cố gắng hiểu ý nghĩa mật mã của hàm boolean trên là gì, và nhanh chóng tìm thấy một "liên hệ". Đây là hàm gì?

../../_images/cast.png

Mở đầu

Bài này mình chứng minh được tính chất của hàm đề bài cho chứ không tìm ra ý nghĩa của hàm nên chỉ được 4/6 điểm.

Tham khảo lời giải của team JPY thì đây là hàm kiểm tra tràn bit số nguyên (integer overflow).

Mình sẽ trình bày cả cách giải của mình lẫn cách giải của team bạn.

Hình đề cho là CAST cipher - tiêu chuẩn mã hóa của Canada.

S-boxes trong CAST cipher được lựa chọn từ các hàm boolean có nonlinearity cao nhất nên trong lúc làm bài mình cắm đầu vào tìm một tính chất gì đó của hàm boolean trên liên quan tới nonlinearity.

Mình đã chứng minh được hai điều:

  • trọng số Hamming của hàm \(f_{2n}\) bằng \(2^{n-1} \cdot (2^n - 1)\);

  • nonlinearity của hàm \(f_{2n}\) bằng \(2^{2n-2}\).

Nhắc lại hàm boolean \(f_{2n}\) đề cho là hàm

\[f_{2n}(x_1, \ldots, x_{2n}) = \bigoplus_{i=1}^n x_i x_{i+n} \prod_{j=i+1}^n (x_j \oplus x_{j+n}).\]

Trọng số Hamming của hàm boolean

Để tìm công thức hoặc tính chất thì thường mình sẽ thử với các số nhỏ và sau đó đoán công thức.

Với \(n = 1\) thì \(f_2(x_1, x_2) = x_1 x_2\). Dễ thấy rằng \(f_2 = 1\) khi \(x_1 = x_2 = 1\).

Thay vì viết bảng chân trị gồm \(4\) dòng thì mình viết thành bảng ô vuông \(2 \times 2\):

\(x_2 = 0\)

\(x_2 = 1\)

\(x_1 = 0\)

\(f_2 = 0\)

\(f_2 = 0\)

\(x_1 = 1\)

\(f_2 = 0\)

\(f_2 = {\color{red}1}\)

Với \(n = 2\) thì hàm boolean là

\[f_4(x_1, x_, x_3, x_4) = \left[x_1 x_3 (x_2 \oplus x_4)\right] \oplus x_2 x_4 = x_1 x_2 x_3 \oplus x_1 x_3 x_4 \oplus x_2 x_4.\]

Cũng tương tự, mình viết bảng chân trị thành bảng ô vuông \(4 \times 4\):

\(x_3 x_4 = 00\)

\(x_3 x_4 = 01\)

\(x_3 x_4 = 10\)

\(x_3 x_4 = 11\)

\(x_1 x_2 = 00\)

\(f_4 = 0\)

\(f_4 = 0\)

\(f_4 = 0\)

\(f_4 = 0\)

\(x_1 x_2 = 01\)

\(f_4 = 0\)

\(f_4 = 0\)

\(f_4 = 0\)

\(f_4 = {\color{red}1}\)

\(x_1 x_2 = 10\)

\(f_4 = 0\)

\(f_4 = 0\)

\(f_4 = {\color{red}1}\)

\(f_4 = {\color{red}1}\)

\(x_1 x_2 = 11\)

\(f_4 = 0\)

\(f_4 = {\color{red}1}\)

\(f_4 = {\color{red}1}\)

\(f_4 = {\color{red}1}\)

Với \(n = 3\) thì mình viết bảng chân trị thành bảng \(8 \times 8\):

\(000\)

\(001\)

\(010\)

\(011\)

\(100\)

\(101\)

\(110\)

\(111\)

\(000\)

\(0\)

\(0\)

\(0\)

\(0\)

\(0\)

\(0\)

\(0\)

\(0\)

\(001\)

\(0\)

\(0\)

\(0\)

\(0\)

\(0\)

\(0\)

\(0\)

\({\color{red}1}\)

\(010\)

\(0\)

\(0\)

\(0\)

\(0\)

\(0\)

\(0\)

\({\color{red}1}\)

\({\color{red}1}\)

\(011\)

\(0\)

\(0\)

\(0\)

\(0\)

\(0\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\(100\)

\(0\)

\(0\)

\(0\)

\(0\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\(101\)

\(0\)

\(0\)

\(0\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\(110\)

\(0\)

\(0\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\(111\)

\(0\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

\({\color{red}1}\)

Đến đây thì mình nhận thấy hàm \(f_{2n}\) sẽ nhận giá trị \(1\) tại:

  • \(1\) ô hàng thứ hai;

  • \(2\) ô hàng thứ ba;

  • ...

  • \(2^n-1\) ô hàng cuối.

Như vậy trọng số Hamming được dự đoán là

\[1 + 2 + \ldots + (2^n - 1) = \dfrac{(2^n - 1) \cdot 2^n}{2} = 2^{n-1} \cdot (2^n - 1).\]

Sau đây mình sẽ chứng minh công thức trọng số Hamming này bằng quy nạp.

Với \(n = 1\), hàm \(f_2(x_1, x_2) = x_1 x_2\) có trọng số Hamming là \(1 = 2^{1-1} \cdot (2^1 - 1)\). Như vậy công thức đúng với \(n = 1\).

Giả sử công thức đúng với \(n = k \geqslant 1\), nghĩa là hàm

\[f_{2k}(x_1, \ldots, x_{2k}) = \bigoplus_{i=1}^k x_i x_{i+k} \prod_{j=i+1}^k (x_j \oplus x_{j+k})\]

có trọng số Hamming là \(2^{k-1} \cdot (2^k - 1)\).

Với \(n = k + 1\), xét hàm

\[f_{2k+2}(x_1, \ldots, x_k, y, x_{k+1}, \ldots, x_{2k}, z) = \bigoplus_{i=1}^k x_i x_{i+k} \left[ \prod_{j=i+1}^k (x_j \oplus x_{j+k}) \cdot (y \oplus z) \right] \oplus yz.\]

Ta có các trường hợp sau:

  • khi \(y = z = 0\) thì \(y \oplus z = 0\)\(yz = 0\). Khi đó mọi phép nhân \(\prod\limits_{j=i+1}^k (x_j \oplus x_{j+k}) \cdot (y \oplus z) = 0\) nên khiến cả hàm \(f_{2k+2} = 0\) tại tất cả vector \((x_1, \ldots, x_{2k})\);

  • khi \(y = 0\)\(z = 1\) thì \(y \oplus z = 1\)\(yz = 0\). Thay vào hàm \(f_{2k+2}\) ta có

\[f_{2k+2}(x_1, \ldots, x_k, 0, x_{k+1}, \ldots, x_{2k}, 1) = \bigoplus_{i=1}^k x_i x_{i+k} \left[ \prod_{j=i+1}^k (x_j \oplus x_{j+k}) \cdot 1 \right] \oplus 0 \equiv f_{2k}(x_1, \ldots, x_{2k}).\]

Như vậy khi \(y = 0\)\(z = 1\) thì \(f_{2k+2}\) chính xác bằng \(f_{2k}\). Nghĩa là trọng số Hamming của \(f_{2k+2}\) lúc này bằng trọng số Hamming của \(f_{2k}\) và bằng \(2^{k-1} \cdot (2^k - 1)\) theo giả thiết quy nạp.

  • khi \(y = 1\)\(z = 0\) thì tương tự, trọng số Hamming của \(f_{2k+2}\) trong trường hợp này cũng bằng \(2^{k-1} \cdot (2^k - 1)\).

  • khi \(y = z = 1\) thì \(y \oplus z = 0\)\(yz = 1\). Khi đó tất cả biểu thức \(\prod\limits_{j=i+1}^k (x_j \oplus x_{j+k}) \cdot (y \oplus z) = 0\) nên hàm \(f_{2k+2} = 1\) với mọi đầu vào \((x_1, \ldots, x_{2k}) \in \mathbb{F}_2^{2k}\). Như vậy trọng số Hamming của \(f_{2k+2}\) ở trường hợp này là \(2^{2k}\).

Kết hợp bốn trường hợp lại thì trọng số Hamming của \(f_{2k+2}\)

\[\mathrm{wt}(f_{2k}) = 2^{k-1} \cdot (2^k - 1) \cdot 2 + 2^{2k} = 2^k (2^{k+1} - 1).\]

Như vậy công thức trọng số Hamming đúng với \(k + 1\) nên theo quy nạp thì công thức đúng với mọi \(n \geqslant 1\).

Biến đổi Walsh-Hadamard và nonlinearity

Gọi \(W_f(\bm{a})\) là hệ số Walsh của hàm boolean \(f\) (trên \(2n\) biến) ứng với vector \(\bm{a} \in \mathbb{F}_2^{2n}\).

Đặt \(\bm{0}\) là vector độ dài \(2n\) chỉ gồm các số \(0\), nghĩa là \(\bm{0} = (0, \ldots, 0)\).

Một tính chất đơn giản của phổ Walsh là

\[W_f(\bm{0}) = 2^{2n} - 2 \cdot \mathrm{wt}(f),\]

với \(f\) là hàm boolean \(2n\) biến.

Từ kết quả về trọng số ở trên, thay vào công thức ta có

\[W_f(\bm{0}) = 2^{2n} - 2 \cdot 2^{n-1} \cdot (2^n - 1) = 2^n.\]

Bây giờ xét hệ số Walsh cho vector dạng

\[\bm{a} = (0, 0, \ldots, a_n, 0, 0, \ldots, a_{2n}).\]

Công thức tính hệ số Walsh là

\[W_f(\bm{a}) = \sum_{\bm{x} \in \mathbb{F}_2^{2n}} (-1)^{f(\bm{x}) \oplus x_n a_n \oplus x_{2n} a_{2n}}.\]

Nếu \(a_n = a_{2n} = 0\) thì ta có trường hợp \(W_f(\bm{0})\) như vừa rồi.

Nếu \(a_n = 1\)\(a_{2n} = 0\) ta sẽ chứng minh hệ số Walsh lúc này có giá trị tuyệt đối lớn nhất. Tương tự đối với \(a_n = 0\)\(a_{2n} = 1\) cũng sẽ cho hệ số Walsh có giá trị tuyệt đối lớn nhất.

Khi \(a_n = 1\)\(a_{2n} = 0\) thì hệ số Walsh sẽ có dạng

\[W_f(\bm{a}) = \sum_{\bm{x} \in \mathbb{F}_2^{2n}} (-1)^{f(\bm{x}) \oplus x_n},\]

ở đây \(\bm{x} = (x_1, \ldots, x_{2n})\).

Ta có hai trường hợp:

Trường hợp 1. Nếu \(x_n = 0\) thì trong số \(2^{2n-1}\) vector \((x_1, \ldots, x_{n-1}, 0, x_{n+1}, \ldots, x_{2n})\), ta cần tìm số lượng vector khiến \(f_{2n} = 1\).

Theo chứng minh về trọng số Hamming ở trên thì nếu \(x_{2n} = 0\) thì hàm \(f_{2n}\) (hiện là hàm \(2n - 2\) biến do \(x_n\)\(x_{2n}\) đã cố định) sẽ luôn có giá trị \(0\). Tuy nhiên chỉ với dữ liệu này thì không đủ để biết có bao nhiêu vector khiến \(f_{2n} = 1\).

Thay đổi cách tiếp cận nhưng xét \(x_{2n} = 1\), khi đó hàm \(f_{2n}\) cũng là hàm \(2n - 2\) biến do \(x_n\)\(x_{2n}\) đã cố định, nhưng hàm \(f_{2n}\) sẽ bằng \(1\) tại đúng \(2^{n-2} \cdot (2^{n-1} - 1)\) vectors trong số \(2^{2n-1}\) vectors đang xét. Vậy theo nguyên lý bù trừ thì \(f_{2n} = 0\) tại

\[2^{2n-1} - 2^{n-2} \cdot (2^{n-1} - 1) = 3 \cdot 2^{2n-3} + 2^{n-2} \ \text{vectors}.\]

Lúc này ta có thể tính

(11)\[\begin{split}\sum_{\substack{\bm{x} \in \mathbb{F}_2^{2n} \\ x_n = 0}} (-1)^{f(\bm{x}) \oplus 0} = (-1)^{0} \cdot \left[ 2^{n-2} \cdot (2^{n-1} - 1) \right] + (-1)^{1} \cdot (3 \cdot 2^{2n-3} + 2^{n-2}) = 2^{2n-2} + 2^{n-1}.\end{split}\]

Trường hợp 2. Nếu \(x_n = 1\), tương tự, trong số \(2^{2n-1}\) vector \((x_1, \ldots, x_{n-1}, 1, x_{n+1}, \ldots, x_{2n})\) ta cần tìm xem có bao nhiêu vector khiến \(f_{2n} = 1\).

Sử dụng chứng minh cho trọng số Hamming ở trên thì

  1. Nếu \(x_{2n} = 0\) thì

\[f_{2n}(x_1, \ldots, x_{n-1}, 1, x_{n+1}, \ldots, 0)\]

bằng \(1\) tại \(2^{n-2} \cdot (2^{n-1} - 1)\) vectors.

  1. Nếu \(x_{2n} = 1\) thì

\[f_{2n}(x_1, \ldots, x_{n-1}, 1, x_{n+1}, \ldots, 1)\]

bằng \(1\) tại tất cả \(2^{2n-2}\) vectors.

Tổng kết lại, hàm \(f_{2n} = 1\) tại

\[A = 2^{n-2} \cdot (2^{n-1} - 1) + 2^{2n-2}\]

vectors. Điều này dẫn tới \(f_{2n} = 0\) tại \(2^{2n-1} - A\) vectors.

Ở đây do \(x_n = 1\) nên khi tính \(\sum (-1)^{f(\bm{x}) \oplus 1}\) ta cần chuyển dấu

(12)\[\begin{split}\sum_{\substack{\bm{x} \in \mathbb{F}_2^{2n} \\ x_n = 1}} (-1)^{f(\bm{x}) \oplus 1} = & (-1) \cdot (2^{2n-1} - A) + 1 \cdot A = 2A - 2^{2n-1} \\ = & 2^{2n-1} + 2^{n-1} \cdot (2^{n-1} - 1) - 2^{2n-1} = 2^{2n-2} - 2^{n-1}.\end{split}\]

Kết hợp (11)(12) ta có hệ số Walsh ứng với vector \(\bm{a}\)\(a_n = 1\)

\[W_f(\bm{a}) = 2^{2n-2} + 2^{n-1} + 2^{2n-2} - 2^{n-1} = 2^{2n-1}.\]

Tương tự, tại vector \(\bm{a}'\)\(a_{2n} = 1\) thì ta cũng có \(W_f(\bm{a}') = 2^{2n-1}\).

Theo định lí Parseval thì

\[\sum_{\bm{a} \in \mathbb{F}_2^n} (W_f(a))^2 = 2^{2 \cdot 2n} = 2^{4n}\]

\(f\) ở đây là hàm boolean \(2n\) biến.

Như vậy ta chỉ cần chứng minh được rằng không tồn tại vector nào khác \(\bm{a}\)\(\bm{a}'\) có hệ số Walsh lớn hơn hoặc bằng \(2^{2n-1}\).

Thật vậy, do tính giao hoán của phép XOR (phép cộng) và phép nhân nên công thức của \(f_{2n}\) cho thấy hai bộ \((x_1, \ldots, x_n)\)\((x_{n+1}, \ldots, x_{2n})\) có thể thay thế nhau, nghĩa là

\[f_{2n}(x_1, \ldots, x_n, x_{n+1}, \ldots, x_{2n}) \equiv f_{2n}(x_{n+1}, \ldots, x_{2n}, x_1, \ldots, x_{n}).\]

Do đó hệ số Walsh cũng có tính đối xứng như vậy. Nếu tồn tại vector \(\bm{b}\) có hệ số Walsh \(W_f(\bm{b}) \geqslant 2^{2n-1}\) thì cũng tồn tại vector \(\bm{b}'\) khác \(\bm{b}\) có hệ số Walsh \(W_f(\bm{b}') \geqslant 2^{2n-1}\).

Khi đó ta có

\[\underbrace{(W_f(\bm{a}))^2}_{=(2^{2n-1})^2} + \underbrace{(W_f(\bm{a}'))^2}_{=(2^{2n-1})^2} + \underbrace{(W_f(\bm{0}))^2}_{=(2^n)^2} + \underbrace{(W_f(\bm{b}))^2}_{> (2^{2n-1})^2} + \underbrace{(W_f(\bm{b}'))^2}_{> (2^{2n-1})^2} > 2^{4n} + 2^{2n}.\]

Điều này vô lý theo đẳng thức Parseval nên không tồn tại vector \(\bm{b}\) có hệ số Walsh lớn hơn hoặc bằng \(2^{2n-1}\).

Như vậy giá trị tuyệt đối lớn nhất trong các hệ số Walsh là \(2^{2n-1}\), từ đó nonlinearity là

\[N_f = 2^{2n-1} - \frac{1}{2} \max \lvert W_f(\bm{a}) \rvert = 2^{2n-1} - \frac{1}{2} \cdot 2^{2n-1} = 2^{2n-1} - 2^{2n-2} = 2^{2n-2}.\]

Như vậy mình đã chứng minh xong công thức cho nonlinearity.

Lời giải của team JPY và gợi ý của thầy Kolomeec

Sau khi hết giải thì mình hỏi ý kiến thầy Kolomeec về cách làm của mình thì có vẻ là không khớp đáp án. Đáp án là nếu ta xem hai vector \((x_1, \ldots, x_n)\)\((x_{n+1}, \ldots, x_{2n})\) như các số nguyên, tức là

\[\begin{split}u & = x_1 + 2 x_2 + \ldots + 2^{n-1} x_n, \\ v & = x_{n+1} + 2 x_{n+2} + \ldots + 2^{n-1} x_{2n},\end{split}\]

thì hàm boolean \(f_{2n}\) có giá trị \(1\) nếu \(u + v \geqslant 2^n\) và ngược lại, đạt giá trị \(0\) nếu \(u + v < 2^n\).

Gợi ý của thầy hoàn toàn khớp với bài giải của team JPY, trong đó nói rằng hàm \(f_{2n}\) là hàm kiểm tra tràn số khi cộng hai số nguyên (integer overflow). Ví dụ khi cộng hai số kiểu unsigned int thì có thể xảy ra hiện tượng tràn bit vì unsigned int chỉ có \(32\) bit. Điều này hợp lý vì CAST cipher có một phép cộng modulo \(2^{32}\), nghĩa là có liên quan đến hình vẽ.

Mình cũng sẽ dùng quy nạp để chứng minh khi \(u + v \geqslant 2^n\) thì \(f_{2n}\) nhận giá trị \(1\).

Với \(n = 1\), hàm \(f_2(x_1, x_2) = x_1 x_2\) nhận giá trị \(1\) khi \(x_1 = x_2 = 1\). Lúc này \(u = x_1 = 1\)\(v = x_2 = 1\) nên \(u + v = 2 \geqslant 2^1\). Như vậy mệnh đề đúng với \(n = 1\).

Giả thiết quy nạp: giả sử mệnh đề đúng với \(n = k \geqslant 1\), nghĩa là hàm boolean

\[f_{2k}(x_1, \ldots, x_k, x_{k+1}, \ldots, x_{2k}) = 1\]

khi \(u + v \geqslant 2^k\) với

\[\begin{split}u & = x_1 + 2 x_2 + \ldots + 2^{k-1} x_k, \\ v & = x_{k+1} + 2 x_{k+2} + \ldots + 2^{k-1} x_{2k}.\end{split}\]

Với \(n = k + 1\), xét hàm

\[f_{2k+2}(x_1, \ldots, x_k, y, x_{k+1}, \ldots, x_{2k}, z) = \bigoplus_{i=1}^k x_i x_{i+k} \left[\prod_{j=i+1}^k (x_j \oplus x_{j+k}) \cdot (y \oplus z)\right] \oplus yz.\]

Mình cũng sẽ có bốn trường hợp cho \((y, z)\):

  1. Khi \(y = z = 0\) thì \(f_{2k+2} = 0\) với mọi \((x_1, \ldots, x_{2k})\) nên chúng ta bỏ qua.

  2. Khi \(y = 1\)\(z = 0\) thì \(f_{2k+2} \equiv f_{2k}\) như chứng minh ở phần trên. Khi đó \(f_{2k+2}\) bằng \(1\) tại các vector \((x_1, \ldots, x_{2k})\) khiến \(f_{2k}\) bằng \(1\), nói cách khác là

\[\begin{split}u' & = x_1 + 2 x_2 + \ldots + 2^{k-1} x_k + 2^k y = u + 2^k y, \\ v' & = x_{k+1} + 2 x_{k+2} + \ldots + 2^{k-1} x_{2k} + 2^k z = v + 2^k z.\end{split}\]

Với \(y = 1\)\(z = 0\) thì

\[u' + v' = u + 2^k y + v = (u + v) + 2^k y \geqslant 2^k + 2^k \cdot 1 = 2^{k+1}.\]

Như vậy nếu \(y = 1\)\(z = 0\) thì mệnh đề cần chứng minh đúng cho \(n = k + 1\).

  1. Khi \(y = 0\)\(z = 1\), chứng minh tương tự trường hợp \(y = 1\)\(z = 0\).

  2. Khi \(y = 1\)\(z = 1\) thì \(f_{2k+2}\) luôn bằng \(1\), tương ứng với

\[u' + v' = u + 2^k y + v + 2^k z = (u + v) + 2^k \cdot (1 + 1) \geqslant 2^k + 2 \cdot 2^k > 2^{k+1}.\]

Như vậy mệnh đề đúng cho \(n = k + 1\) với mọi trường hợp \((y, z)\) và ta có điều cần chứng minh.

Open competition: NSUCRYPTO lightweight cipher

Đây là bài 6 ở round 2.

Đề bài

Ghi chú

Problem for special prize

NSUCRYPTO team tổ chức cuộc thi phát triển mã khối light-weight mới.

Một số yêu cầu:

  • Block size là \(64\) bits.

  • Key size là \(80\), \(96\) hoặc \(64\) bits.

  • Số vòng là \(32\).

  • Cấu trúc xây dựng tùy ý. Nghĩa là SPN, ARX, Feistel đều có thể sử dụng, thậm chí là một cách xây dựng mới.

Thí sinh cần xem xét PRESENT light-weight cipher (2007) và cố gắng tìm ra điều có thể làm tốt hơn mã khối này.

Hãy so sánh đáp án của bạn với PRESENT: khi thực hiện, khi chống phá mã (linear, differential, algebraic, ...) và đưa ra các giải thích cần thiết.

Lời giải

Đây là bài có tính mở rất cao nhưng để tìm ra điều gì đó mới (và hiệu quả) trong 7 ngày là rất khó. Do đó lúc thi mình đã xây dựng một block cipher mới tốt hơn PRESENT về mặt lưu trữ, còn các tính chất khác như kháng phá mã thì mình không đề cập tới. Bài này mình được 6/10 điểm.

Về PRESENT thì mình đã có tóm tắt trong một bài viết của blog này nên mình không viết lại. PRESENT cipher là thuật toán light-weight, tức là thuật toán được thiết kế nhỏ gọn để chạy trên các thiết bị có bộ xử lý và lưu trữ nhỏ (như các hệ thống nhúng).

Cải tiến của mình dựa trên việc PRESENT sử dụng mạng SP nên sẽ cần lưu trữ phép biến đổi để mã hóa và phép biến đổi ngược để giải mã:

  • cần lưu trữ đồng thời S-box là \(S[x]\) và inverse S-box là \(S^{-1}[x]\);

  • cần lưu trữ biến đổi tuyến tính pLayer và biến đổi ngược của nó;

  • addRoundKey có phép biến đổi ngược là chính nó nên mình bỏ qua.

Như vậy, ta tốn hai phần bộ nhớ: một cho phép biến đổi mã hóa và một cho phép biến đổi ngược để giải mã.

Làm sao để cải tiến? Mình dùng mô hình Feistel, hoặc mô hình Feistel tổng quát (generalize Feistel network) để xây dựng mã khối mới.

Khối đầu vào (plaintext) có \(64\) bits sẽ được chia thành \(4\) phần bằng nhau, mỗi phần có \(16\) bits:

\[P = (X^0, X^1, X^2, X^3).\]

Khóa \(K\)\(64\) bits sẽ sinh ra các khóa con \(K_0\), \(K_1\), ..., \(K_{31}\) cho \(32\) vòng.

Tại vòng thứ \(i\) với \(0 \leqslant i \leqslant 31\), \(X^{i+4}\) được tính như sau

\[X^{i+4} = X^i \oplus F(X^{i+1} \oplus X^{i+2} \oplus X^{i+3}, K_i),\]

trong đó \(F: \mathbb{F}_2^{16} \times \mathbb{F}_2^{16} \to \mathbb{F}_2^{16}\) là round function.

../../_images/solution-6.jpg

Ciphertext sau \(32\) vòng sẽ là trạng thái cuối viết theo thứ tự ngược lại, nghĩa là

\[C = (X^{35}, X^{34}, X^{33}, X^{32}).\]

Round function. Round function \(F: \mathbb{F}_2^{16} \times \mathbb{F}_2^{16} \to \mathbb{F}_2^{16}\) gồm ba phép biến đổi là cộng modulo \(2^{16}\), S-box và linear layer.

  1. Phép cộng modulo \(2^{16}\), đặt \(Y = X^i \oplus X^{i+1} \oplus X^{i+2}\), khi đó ta tính \(Y + K_i \pmod{2^{16}}\) và kí hiệu là \(Y \boxplus K_i\).

  2. S-box là ánh xạ \(S: \mathbb{F}_2^4 \to \mathbb{F}_2^4\), trong đó \(16\) bits từ phép cộng trên được chia thành \(4\) phần, mỗi phần có \(4\) bits đi qua S-box. Sau đó ta nối các kết quả lại thành \(16\) bits mới.

  3. Linear layer \(L\). Ta xor \(X\) với \(X \lll 11\), trong đó \(\lll\) là phép dịch \(11\) bits sang trái.

Như vậy

\[S(X) = S(a_0, a_1, a_2, a_3) = (\mathtt{Sbox}(a_0), \mathtt{Sbox}(a_1), \mathtt{Sbox}(a_2), \mathtt{Sbox}(a_3)),\]

với \(X = (a_0, a_1, a_2, a_3) \in \mathbb{F}_2^{16}\)\(a_i \in \mathbb{F}_2^4\).

S-box được cho bởi bảng sau

\(x\)

\(0\)

\(1\)

\(2\)

\(3\)

\(4\)

\(5\)

\(6\)

\(7\)

\(8\)

\(9\)

\(\text{A}\)

\(\text{B}\)

\(\text{C}\)

\(\text{D}\)

\(\text{E}\)

\(\text{F}\)

\(S(x)\)

\(\text{C}\)

\(0\)

\(\text{F}\)

\(\text{A}\)

\(2\)

\(\text{B}\)

\(9\)

\(5\)

\(8\)

\(3\)

\(\text{D}\)

\(7\)

\(1\)

\(\text{E}\)

\(6\)

\(4\)

Linear layer như sau:

\[L(X) = X \oplus (X \lll 11).\]

Round key. Khóa đầu vào \(K\)\(64\) bits sẽ được chia thành \(8\) phần \(K_0\), \(K_1\), ..., \(K_7\). Các vòng dùng khóa con như sau:

  1. Các vòng 0, 1, ..., 7 sử dụng lần lượt các khóa \(K_0\), \(K_1\), ..., \(K_7\).

  2. Các vòng 8, 9, ..., 15 sử dụng lần lượt các khóa \(K_0\), \(K_1\), ..., \(K_7\).

  3. Các vòng 16, 17, ..., 23 sử dụng lần lượt các khóa \(K_0\), \(K_1\), ..., \(K_7\).

  4. Các vòng 24, 25, ..., 31 sử dụng lần lượt các khóa \(K_7\), \(K_6\), ..., \(K_0\).

Decryption. Đẻ giải mã ta thực hiện theo thứ tự ngược lại. Giả sử ciphertext là

\[C = (X^{35}, X^{34}, X^{33}, X^{32}),\]

ta viết theo thứ tự ngược lại là

\[C_{32} = (X^{32}, X^{33}, X^{34}, X^{35}).\]

Tại vòng thứ \(i\) với \(i = 31, \ldots, 0\), \(X^i\) sẽ được tính bởi

\[X^i = X^{i+4} \oplus F(X^{i+1} \oplus X^{i+2} \oplus X^{i+3}, K_i),\]

trong đó \(F\) là round function được định nghĩa ở trên. Cuối cùng plaintext là

\[P = (X^0, X^1, X^2, X^3).\]

A nonlinear generator

Đây là bài 4 ở round 1 và bài 7 ở round 2.

Đề bài

Alice tạo ra một bộ sinh khóa như hình sau:

../../_images/problem-7.png

Bộ sinh khóa gồm hai thanh ghi độ dài \(47\)\(49\) với các hàm không tuyến tính (non-linear feedback).

Tại mỗi thời điểm \(t = 1,2,\ldots\), mỗi thanh ghi sẽ sinh keystream tại trạng thái ở thời điểm \(t\) và sau đó chuyển sang trạng thái ở thời điểm \(t+1\).

Cụ thể, trạng thái của thanh ghi ở thời điểm \(t\)

\[A(t) = (a_1(t), \ldots, a_{47}(t)) \, \text{và} \, B(t) = (b_1(t), \ldots, b_{49}(t)).\]

Hai thanh ghi được dịch chuyển đồng thời. Ví dụ

\[A(t+1) = (a_1(t), \ldots, a_{47}(t), (a_1(t) \& a_2(t)) \oplus a_{13}(t) \oplus a_{44}(t)).\]

Keystream \(\gamma\) với độ dài \(8192\) tạo bởi bộ sinh khóa trên được ghi lại trong file keystream.txt. Ngoài ra chúng ta cũng có trạng thái \(A(8192)\)\(B(8192)\) là:

\[\begin{split}A(8192) & = (00101001110001001110111001100001010100000101110), \\ B(8192) & = (0000010000101001000011000001010111001110000100101).\end{split}\]

Bạn có thể tìm lại trạng thái khởi tạo \(A(1)\)\(B(1)\) không?

Lời giải

Bài này chúng ta bruteforce từ cặp bit \(A(i)\)\(B(i)\) bắt đầu từ vị trí \(8191\) về \(1\).

Mình sẽ viết lại dãy của đề bài. Dãy trên và dãy dưới lần lượt là \(a_0, a_1, \ldots\)\(b_0, b_1, \ldots\) và các phần tử sau được sinh ra từ các phần tử trước theo công thức

\[a_{i+47} = (a_i \& a_{i+1}) \oplus a_{i+12} \oplus a_{i+43}\]

\[b_{i+49} = (b_i \& b_{i+1}) \oplus b_{i+10} \oplus b_{i+47}.\]

Keystream \(\{ g_n \}\) sẽ được sinh theo công thức

\[\begin{split}g_i & = a_i \oplus a_{i+3} \oplus (a_{i+7} \& a_{i+8}) \\ & \oplus b_i \oplus (b_{i+4} \& b_{i+5}) \oplus b_{i+7}.\end{split}\]

Như vậy mình sẽ bruteforce từ vị trí \(i\) từ \(8190\) về \(0\). Ở mỗi vị trí mình có hai trường hợp \(a_i\) và hai trường hợp \(b_i\). Nếu đặt được tới \(i = 0\) thì đây là dãy cần tìm.

Keystream là một chuỗi \(8192\) bits nên mình sẽ lưu ở file keystream.txt.

from itertools import product
import sys

sys.setrecursionlimit(100000)

g = list(map(int, open("keystream.txt").read()))

def check_a(val, x, i):
    return x[i+47] == (val & x[i+1]) ^ x[i+12] ^ x[i+43]

def check_b(val, y, i):
    return y[i+49] == (val & y[i+1]) ^ y[i+10] ^ y[i+47]

def check_g(valx, valy, x, y, z, i):
    return z[i] == valx ^ x[i+3] ^ (x[i+7] & x[i+8]) ^ valy ^ (y[i+4] & y[i+5]) ^ y[i+7]

a = [-1] * 8191 + list(map(int, '00101001110001001110111001100001010100000101110'))
b = [-1] * 8191 + list(map(int, '0000010000101001000011000001010111001110000100101'))

def bruteforce(i):
    if i == -1:
        assert "".join(map(str, a[:47])) == "01111100001110000110010001011000110000110011110"
        assert "".join(map(str, b[:49])) == "1100110011010001010101000101110100011011010010110"
        return
    else:
        for ai, bi in product(range(2), repeat=2):
            if check_a(ai, a, i) and check_b(bi, b, i) and check_g(ai, bi, a, b, g, i):
                a[i], b[i] = ai, bi
                bruteforce(i-1)
                a[i], b[i] = -1, -1

bruteforce(8190)

Bài này được giải bởi bạn Uyên và là BEST SOLUTION. Điều thú vị là rất nhiều đội được BEST SOLUTION cho bài này :)))

Unsecure SP-network

Đây là bài 5 ở round 1 và bài 8 ở round 2.

Đề bài

Bob có nghe về mạng SP và quyết định xây dựng mật mã của riêng mình.

Block size (kích thước khối) là \(32\) bits. Bob tạo S-boxes kích thước \(2 \times 2\)\(P\)-layer sử dụng một phần của secret key.

Ở đây \(P\) là một biến đổi tuyến tính \(P : \mathbb{F}_2^{32} \to \mathbb{F}_2^{32}\), nghĩa là

\[P(x \oplus y) = P(x) \oplus P(y)\]

với mọi \(x, y \in \mathbb{F}_2^{32}\).

Ngoài ra, secret key là \(K \in \mathbb{F}_2^{128}\). Với nó Bob đặt

\[K^i = (K_{32 (i \bmod 4) + 1}, \ldots, K_{32(i \bmod 4) + 32})\]

có độ dài \(32\) với mọi \(i \in \{ 1, \ldots, 100 \}\).

Như vậy vòng thứ \(i\) của mã khối sẽ như sau:

\[r_i(x) = P(S(x_1 \oplus K_1^i, x_2 \oplus K_2^i), S(x_3 \oplus K_3^i, x_4 \oplus K_4^i), \ldots, S(x_{31} \oplus K_{31}^i, x_{32} \oplus K_{32}^i))\]

với \(x \in \mathbb{F}_2^{32}\).

Ciphertext sẽ là

\[c = K^{100} \oplus r_{99}(r_{98}(\ldots r_1(m)))\]

với \(m\) là plaintext ban đầu, \(m = (m_1, \ldots, m_{32})\)\(m_i \in \mathbb{F}_2\).

Tuy nhiên Bob nhanh chóng nhận ra Carol có thể đọc được plaintext mà không gặp vấn đề nếu Carol biết \(100\) cặp plaintext và ciphertext bất kì.

Tại sao cô ấy làm được?

Lời giải

Giả sử việc mã hóa có dạng \(C = \mathtt{Enc}(P)\). Mình sẽ chứng minh rằng với hai plaintext \(P\)\(P'\), nếu ciphertext tương ứng là \(C\)\(C'\) thì

\[C \oplus C' = \mathtt{Enc}(P \oplus P').\]

Một điều khá buồn cười là bài này mình mất kha khá thời gian vì những suy nghĩ ... vĩ mô. Theo đề bài có thể nhận thấy S-box \(S\) là ánh xạ từ \(\mathbb{F}_2^2\) tới \(\mathbb{F}_2^2\). Theo quy tắc nhân có tất cả \(2^{2^2} = 16\) ánh xạ từ \(\mathbb{F}_2^2\) tới \(\mathbb{F}_2\) (số lượng hàm boolean hai biến). Do đó có \(16^2 = 256\) trường hợp S-box \(S\).

Đối với các S-box tuyến tính hoặc affine thì câu chuyện không phải vấn đề. Tuy nhiên các trường hợp khác rất khó kiểm soát. Lúc này bạn Uyên nói với mình có \(4! = 24\) trường hợp của S-box thôi, là số lượng hoán vị trên tập \(\{ 0, 1, 2, 3 \}\). Lý do là vì thuật toán mã hóa sử dụng mạng SP, do đó mỗi phép biến đổi phải có phép biến đổi ngược. Chân lý đây rồi, ánh sáng đây rồi :v :v :v Vậy mà mình nghĩ mãi, haizz.

Ý tưởng của mình là sử dụng phá mã vi sai (differential cryptanalysis).

Với ánh xạ \(M: \mathbb{F}_2^n \to \mathbb{F}_2^n\) mình gọi:

  1. Input differential\(x_1 \oplus x_2\).

  2. Output differential\(M(x_1) \oplus M(x_2)\).

Mình dùng SageMath để kiểm tra nhanh phân bố vi sai trên \(24\) hoán vị (ứng với \(24\) S-box).

import itertools
from sage.crypto.sbox import SBox

for perm in itertools.permutations(range(4)):
    print(f"S-box: {perm}")
    print(SBox(perm).difference_distribution_table())
    print()

Một dấu hiệu chung cho mọi S-box là nếu input differential cố định thì output differential cũng cố định.

Ví dụ, với S-box tương ứng với hoán vị

\[\begin{split}S = \begin{pmatrix} 0 & 1 & 2 & 3 \\ 1 & 2 & 0 & 3 \end{pmatrix}\end{split}\]

thì bảng phân bố vi sai là

\(00\)

\(01\)

\(10\)

\(11\)

\(00\)

\(\color{red}{4}\)

\(0\)

\(0\)

\(0\)

\(01\)

\(0\)

\(0\)

\(0\)

\(\color{red}{4}\)

\(10\)

\(0\)

\(\color{red}{4}\)

\(0\)

\(0\)

\(11\)

\(0\)

\(0\)

\(\color{red}{4}\)

\(0\)

Theo bảng trên, nếu input differential là \(10\) thì output differential chắc chắn là \(01\).

Như vậy mình có nhận xét sau.

Nhận xét 12

Với S-box bất kì là hoán vị trên tập \(\{ 0, 1, 2, 3 \}\), nếu input differential cố định thì output differential cũng cố định.

Đầu tiên mình cần viết lại các kí hiệu trong bài này.

Plaintext có \(32\) bit:

\[m = (m_1, \ldots, m_{32}), \quad m_i \in \mathbb{F}_2.\]

S-box là hoán vị trên tập \(\{ 0, 1, 2, 3 \}\). Nói cách khác S-box là song ánh \(S\) từ \(\mathbb{F}_2^2\) tới \(\mathbb{F}_2^2\).

Đặt \(\mathcal{S}\) là ánh xạ biến đổi \(32\) bit thành \(32\) bit mới với S-box, nghĩa là

\[\begin{split}S : & \ \mathbb{F}_2^{32} \times \mathbb{F}_2^{32} \to \mathbb{F}_2^{32} \\ & (x, K) \mapsto X,\end{split}\]

trong đó:

  • \(x = (x_1, \ldots, x_{32})\) là khối \(32\) bit

  • \(K = (K_1, \ldots, K_{32})\) là khóa con ở vòng tương ứng

  • \(X = (X_1, \ldots, X_{32})\) nhận được từ

\[\begin{split}\begin{array}{ccl} (X_1, X_2) & = & S(x_1 \oplus K_1, x_2 \oplus K_2), \\ (X_3, X_4) & = & S(x_3 \oplus K_3, x_4 \oplus K_4), \\ \cdots \\ (X_{31}, X_{32}) & = & S(x_{31} \oplus K_{31}, x_{32} \oplus K_{32}). \end{array}\end{split}\]

Biến đổi tuyến tính là ánh xạ \(P\) từ \(\mathbb{F}_2^{32}\) tới \(\mathbb{F}_2^{32}\).

Secret key là \(K \in \mathbb{F}_2^{128}\). Subkey ở vòng thứ \(i\) nhận được từ \(K\) theo công thức

\[K^i = (K_{32 (i \bmod 4) + 1}, \ldots, K_{32 (i \bmod 4) + 32}),\]

với \(i \in \{ 1, \ldots, 100 \}\). Mỗi subkey \(K^i\) có độ dài \(32\) bit.

Biến đổi ở vòng thứ \(i\)

\[r_i(x) = P(\mathcal{S}(x, K^i)).\]

Ciphertext sau \(100\) vòng sẽ là

\[c = K^{100} r_{99}(r_{98}(\ldots (r_1(m)))).\]

Tiếp theo, sử dụng differential attack mình sẽ tìm liên hệ known-plaintext. Mình xét vòng đầu tiên trước.

Giả sử mình có hai plaintext là

\[m = (m_1, \ldots, m_{32}) \ \text{và} \ m' = (m_1', \ldots, m_{32}') \in \mathbb{F}_2^{32}.\]

Ta có

\[\begin{split}(m_1, m_2) \rightarrow S(m_1 \oplus K_1^1, m_2 \oplus K_2^1) = (X_1, X_2), \\ (m_1', m_2') \rightarrow S(m_1' \oplus K_1^1, m_2' \oplus K_2^1) = (X_1', X_2').\end{split}\]

Input differential cho S-box là

\[(m_1 \oplus K_1^1 \oplus m_1' \oplus K_1^1, m_2 \oplus K_2^1 \oplus m_2' \oplus K_2^1) = (m_1 \oplus m_1', m_2 \oplus m_2').\]

Ở đây subkey không ảnh hưởng input differential và đây là yếu tố quan trọng cần chú ý.

Nếu mình cố định input differential \((m_1 \oplus m_1', m_2 \oplus m_2')\) thì output differential

\[S(m_1 \oplus K_1^1, m_2 \oplus K_2^1) \oplus S(m_1' \oplus K_1^1, m_2' \oplus K_2^1) = (X_1, X_2) \oplus (X_1', X_2) = (X_1 \oplus X_1', X_2 \oplus X_2')\]

cũng cố định. Tương tự cho các cặp còn lại \((m_3, m_4)\), ..., \((m_{31}, m_{32})\).

Như vậy ở vòng đầu ta có

\[\begin{split}& r_1(m) = P(\mathcal{S}(m, K^1)) = P(X_1, X_2, \ldots, X_{32}), \\ & r_1(m') = P(\mathcal{S}(m', K^1)) = P(X_1', X_2', \ldots, X_{32}').\end{split}\]

Suy ra

\[r_1(m) \oplus r_1(m') = P(X_1, \ldots, X_{32}) \oplus P(X_1', \ldots, X_{32}') = P(X_1 \oplus X_1', \ldots, X_{32} \oplus X_{32}')\]

\(P\) là biến đổi tuyến tính. Rõ ràng nếu \(X_i \oplus X_i'\) cố định với mọi \(i = 1, \ldots, 32\) thì kết quả \(r_1(m) \oplus r_1(m')\) cũng cố định theo.

Áp dụng kết quả này cho \(99\) vòng. Ở vòng cuối (vòng \(100\)) ta XOR với \(K^{100}\) nhưng thực ra là

\[\begin{split}c \oplus c' & = (K^{100} \oplus r_{99}(r_98)(\ldots r_1(m))) \oplus (K^{100} \oplus r_{99}(r_{98}(\ldots r_1(m')))) \\ & = r_{99}(r_98)(\ldots r_1(m)) \oplus r_{99}(r_{98}(\ldots r_1(m'))),\end{split}\]

tức là \(K^{100}\) cũng không ảnh hưởng differential như \(K_1, \ldots, K_{99}\).

Tóm lại mình nhận được kết quả sau.

Nhận xét 13

Nếu ta biết plaintext \(P\) và ciphertext tương ứng là \(C\), thì với mọi ciphertext \(C'\) mới nào đó ta luôn có thể tìm lại được plaintext tương ứng. Nói cách khác là chú ý ở đầu bài

\[C \oplus C' = \mathtt{Enc}(P \oplus P').\]

Chứng minh

Đặt \(DDT_S\) là ánh xạ từ input differential thành output differential của S-box. Do input differential cố định thì output differential, \(DDT_S\) sẽ nhận được từ bảng phân bố vi sai bằng cách, nếu phần tử ở hàng \(i\) và cột \(j\) bằng \(4\) thì \(DDT_S(i) = j\).

VÍ dụ, với S-box

\[\begin{split}S = \begin{pmatrix} 0 & 1 & 2 & 3 \\ 1 & 2 & 0 & 3 \end{pmatrix}\end{split}\]

như trên thì

\[\begin{split}DDT_S = \begin{pmatrix} 0 & 1 & 2 & 3 \\ 0 & 3 & 1 & 2 \end{pmatrix}.\end{split}\]

Rõ ràng \(DDT_S\) là hoán vị, hay song ánh. Do đó tồn tại biến đổi ngược

\[\begin{split}DDT_S^{-1} = \begin{pmatrix} 0 & 1 & 2 & 3 \\ 0 & 2 & 3 & 1 \end{pmatrix}.\end{split}\]

Đặt \(P^{-1}\) là nghịch đảo của \(P\).

Giả sử \(c = (c_1, \ldots, c_{32})\)\(c' = (c_1', \ldots, c_{32}')\) là các ciphertexts.

Đặt \(P^{-1}(c \oplus c') = (C_1, \ldots, C_{32})\). Áp dụng \(DDT_S^{-1}\) ta có

\[DDT_S^{-1}(C_1, C_2), DDT_S^{-1}(C_3, C_4), \ldots, DDT_S^{-1}(C_{31}, C_{32}),\]

và tương tự cho \(98\) vòng còn lại. Cuối cùng ta sẽ nhận được differential của các plaintexts \(P\)\(P'\).

Do đã biết \(P\) và differential \(P \oplus P'\) vừa tìm được, ta có thể tìm \(P'\).

# binmatrix.py

# https://github.com/xiangzejun/binary_matrix
# For feedback or questions, pleast contact at xiangzejun@iie.ac.cn

# Implemented by Xiang Zejun, State Key Laboratory of Information Security,
# Institute Of Information Engineering, CAS

from functools import reduce

class DataError(Exception):
    """
    Define my data exception.
    Check whether the elements of the matrxi are binaries.
    """
    def __init__(self, x, y):
        """
        store the coordinate of the entry which is not binary.
        """
        self.x = x
        self.y = y

    def printError(self):
        print("The element at [{0}][{1}] is NOT binary!".format(self.x, self.y))

class FormatError(Exception):
    """
    Define my format exception.
    Check whether input is a matrix or a square matrix.
    """
    def __init__(self, s):
        self.error = "The input is " + s
    def printError(self):
        print(self.error)

class RankError(Exception):
    """
    Define my rank exception.
    Check whether the square matrix is full rank when calculating its inverse.
    """
    def __init__(self, r):
        self.r = r
    def printError(self):
        print("The matrix is NOT full rank. (rank = {0})".format(self.r))

class BinMatrix:
    def __init__(self, m = [[1]]):
        """
        Initilize a matrix.
        """
        self.m = m
        self.r_len = len(self.m) # row number
        self.c_len = len(self.m[0]) # column number
        # self.length = len(self.m)

    def __convertMatrixToInt(self):
        """
        Convert each row of the binary matrix to an integer.
        """
        return [int(reduce(lambda x , y: x + y, map(str, self.m[i])), 2)
            for i in range(self.r_len)]

    def __appendUnitMatrix(self):
        """
        Append a unit matrix to m_int.
        """
        m_int = self.__convertMatrixToInt()
        return [(1 << (self.r_len + self.c_len - 1 - i)) ^ m_int[i]
            for i in range(self.r_len)]

    def __chooseElement(self, r, c, m_int):
        """
        Choose a none-zero row started from position [r][c].
        """

        err = "The row index can not exceed the column index in row-reduced echelon matrix."
        assert r <= c, err

        if c == self.c_len:
            return None
        else:
            mask = (1 << (self.c_len - 1 - c))
            temp = [(m_int[i] & mask) for i in range(r, self.r_len)]
            if mask not in temp:
                return self.__chooseElement(r, c + 1, m_int)
            else:
                return (temp.index(mask) + r, c)

    @staticmethod
    def __switchRows(r1, r2, m_int):
        """
        Switch r1-th and r2-th rows of m_int.
        """
        temp = m_int[r1]
        m_int[r1] = m_int[r2]
        m_int[r2] = temp

    def __addRows(self, r, c, m_int):
        """
        Add the r-th row to all the other rows if the c-th element of
        the corresponding rows are nonzero.
        """
        mask = (1 << (self.c_len - 1 - c))
        it = list(range(self.r_len))
        it.remove(r)
        for i in it:
            if m_int[i] & mask != 0:
                m_int[i] ^= m_int[r]


    def __isMatrix(self):
        """
        Check whether the input is a matrix.
        """
        if [len(l) for l in self.m].count(self.c_len) != self.r_len:
            raise FormatError("NOT a matrix!")
        else:
            pass

    def __isSquareMatrix(self):
        """
        Check whether the input is a square matrix.
        """
        if [len(l) for l in self.m].count(self.r_len) != self.r_len:
            raise FormatError("NOT a Square matrix!")
        else:
            pass

    def __isBinary(self):
        """
        Check whether the entrys in the input are binaries.
        """
        for i in range(len(self.m)):
            for j in range(len(self.m[i])):
                if self.m[i][j] not in [0,1]:
                    raise DataError(i, j)
                else:
                    pass

    def rank(self):
        """
        Calculate the Rank of the matrix.
        """
        self.__isMatrix()
        self.__isBinary()
        m_int = self.__convertMatrixToInt()
        r = 0
        c = 0
        for i in range(self.r_len):
            arg = self.__chooseElement(r, c, m_int)
            if arg != None:
                r_temp = arg[0]
                c = arg[1]
                self.__switchRows(r, r_temp, m_int)
                self.__addRows(r, c, m_int)
                r += 1
                c += 1
            else:
                return r
        return self.r_len

    def det(self):
        """
        Calculate the Det of the matrix.
        """
        self.__isSquareMatrix()
        self.__isBinary()
        if self.rank() == self.r_len:
            return 1
        else:
            return 0

    def inv(self):
        """
        Calculate the inverse of the matrix.
        """
        self.__isSquareMatrix()
        self.__isBinary()
        m_adj = self.__appendUnitMatrix()
        r = 0
        c = 0
        for i in range(self.r_len):
            arg = self.__chooseElement(r, c, m_adj)
            if arg != None:
                r_temp = arg[0]
                c = arg[1]
                self.__switchRows(r, r_temp, m_adj)
                self.__addRows(r, c, m_adj)
                r += 1
                c += 1
            else:
                raise RankError(r)
        return [
            list(map(int, list(format((m_adj[i] >> self.c_len), "0" + str(self.r_len) + "b"))))
            for i in range(self.r_len)]
# cipher.py

from binmatrix import BinMatrix
import numpy as np
import random

sbox = list(range(4))
random.shuffle(sbox)    # generate random S-box which is permuation

P = None
while True: # generate random invertible matrix on F_2
    P = np.random.randint(2, size=(32, 32))
    if np.linalg.det(P) % 2 == 1:
        break

P = [row.tolist() for row in P] # convert from numpy.narray to list
P_inv = BinMatrix(P).inv()      # calculate inverse of P

difference_distribution_table = [[0 for _ in range(4)] for __ in range(4)]
# i-th row -> input differential
# o-th row -> output differential
for a in range(4):
    for b in range(4):
        i = a ^ b
        o = sbox[a] ^ sbox[b]
        difference_distribution_table[i][o] += 1

sbox_diff = [0] * 4
# calculate DDT_S
for i in range(4):
    for j in range(4):
        if difference_distribution_table[i][j] == 4:
            sbox_diff[i] = j

sbox_diff_inv = [sbox_diff.index(i) for i in range(4)] # DDT_S^{-1}

def matmul(M: list[list[int]], v: list[int]) -> list[int]:
    # multiplication of matrix M and vector v
    result = [0] * len(v)
    for i in range(len(v)):
        result[i] = sum(M[i][j] * v[j] for j in range(len(v))) % 2
    return result

def encrypt(m: list[int], K: list[int]) -> list[int]:
    # encryption with plaintext m, key K
    Ki = [K[i:i+32] for i in range(0, len(K), 32)]
    pt = m.copy()
    for i in range(99):
        pt = [p ^ k for p, k in zip(pt, Ki[i % 4])]
        for j in range(0, len(pt), 2):
            z = (pt[j] << 1) + pt[j+1]  # get two bits for input of S-box
            z = sbox[z]
            pt[j] = z >> 1      # write two new bits
            pt[j+1] = z & 1     # after S-box
        pt = matmul(P, pt)
    pt = [p ^ k for p, k in zip(pt, Ki[100 % 4])]
    return pt
# solve.py

import random
from cipher import *

pt1 = random.choices(range(2), k = 32)  # Known
pt2 = random.choices(range(2), k = 32)  # Unknown

K = random.choices(range(2), k = 128)

ct1 = encrypt(pt1, K)   # Known
ct2 = encrypt(pt2, K)   # Known

ct_diff = [x ^ y for x, y in zip(ct1, ct2)] # ct1 ^ ct2

for _ in range(99):
    ct_diff = matmul(P_inv, ct_diff)
    for i in range(0, len(ct_diff), 2):
        z = (ct_diff[i] << 1) + ct_diff[i+1]
        z = sbox_diff_inv[z]
        ct_diff[i] = z >> 1
        ct_diff[i+1] = z & 1

pt2_guess = [x^y for x, y in zip(ct_diff, pt1)] # Recover pt2
assert pt2 == pt2_guess

Một điều thú vị là có thể dùng nhiều S-box và quá trình tấn công vẫn tương tự. Ví dụ

\[(x_1, \ldots, x_{32}) \xrightarrow{K^i} (S_1(x_1 \oplus K_1^i, x_2 \oplus K_2^i), S_2(x_3 \oplus K_3^i, x_4 \oplus K_4^i), \ldots, S_{16}(x_31 \oplus K_{31}^i, x_{32} \oplus K_{32}^i)),\]

với \(S_i\) là hoán vị bất kì trên \(\{ 0, 1, 2, 3 \}\), \(i = 1, \ldots, 16\).

Ở đây ta sẽ áp dụng cùng phương pháp như trên nhưng ta sẽ tính \(DDT_{S_i}\) cho mỗi S-box \(S_i\).

Lời giải hoàn chỉnh

Thật ra mình đã đọc thiếu đề và chưa xét trường hợp S-box và \(P\) là các phép biến đổi bí mật (giống secret key). Đây là lý do mình không được full điểm câu này.

S-box, theo phân tích của mình ở trên, thực chất là một ánh xạ affine. Nhắc lại ánh xạ affine là ánh xạ có dạng \(S(\bm{x}) = \bm{A} \cdot \bm{x} \oplus \bm{b}\), trong đó \(\bm{A}\) là ma trận, \(\bm{x}\)\(\bm{b}\) là các vector cột.

Khi đó, với hai đầu vào \(\bm{x}\)\(\bm{x}'\) sao cho \(\bm{x} \oplus \bm{x}'\) cố định (input differential) thì ta có

\[S(\bm{x}) \oplus S(\bm{x}') = \bm{A} \cdot (\bm{x} \oplus \bm{x}')\]

cũng cố định do \(\bm{A}\) đã xác định.

Theo đó toàn bộ quá trình encrypt là một ánh xạ affine lớn, do đó chúng ta hoàn toàn có thể tìm được ánh xạ affine đó nếu biết đủ \(32\) vector độc lập tuyến tính.

Sử dụng cipher.py ở trên và đoạn code sau để giải:

import random
from sage.all import random_vector, vector, GF, matrix, ones_matrix
from cipher import encrypt

random.seed(1333)

K = [random.randrange(0, 2) for _ in range(128)]
ms = [random_vector(GF(2), 32) for _ in range(100)]

cs = [encrypt(list(map(int, m)), K) for m in ms]

ms = matrix(GF(2), ms)
cs = matrix(GF(2), cs)
A = matrix(GF(2), 32, 32)
b = vector(GF(2), 32)

for i in range(32):
    for bb in range(2):
        try:
            res = ms.solve_right(cs[:, i] + bb * ones_matrix(GF(2), 100, 1))
            A[:, i] = res
            b[i] = bb
        except:
            pass

u = random_vector(GF(2), 32)
assert encrypt(list(map(int, u)), K) == list(map(int, u * A + b))

Ở đây mình viết plaintext ở dạng hàng. Giả sử plaintext và ciphertext là

\[M = (m_1, \ldots, m_n), \quad C = (c_1, \ldots, c_n).\]

Ánh xạ affine của chúng ta tương ứng với

\[(c_1, \ldots, c_n) = (m_1, \ldots, m_n) \cdot \bm{A} + (b_1, \ldots, b_n),\]

trong đó \(\bm{A}\) là ma trận \(n \times n\) (trong trường hợp bài này \(n = 32\)).

Như vậy mình cần tìm ma trận \(\bm{A}\) và vector \(\bm{b} = (b_1, \ldots, b_n)\). Nếu có \(T\) phương trình như vậy

\[\begin{split}(c_{11}, \ldots, c_{1n}) & = (m_{11}, \ldots, m_{1n}) \cdot \bm{A} + (b_1, \ldots, b_n), \\ (c_{21}, \ldots, c_{2n}) & = (m_{21}, \ldots, m_{2n}) \cdot \bm{A} + (b_1, \ldots, b_n), \\ \cdots & = \cdots \\ (c_{T1}, \ldots, c_{Tn}) & = (m_{T1}, \ldots, m_{Tn}) \cdot \bm{A} + (b_1, \ldots, b_n),\end{split}\]

thì mình giải từng cột thứ \(i\) của ma trận \(A\) và từng số hạng \(b_i\). Nghĩa là

\[\begin{split}\begin{pmatrix} c_{11} \\ c_{12} \\ \vdots \\ c_{T1} \end{pmatrix} = \begin{pmatrix} m_{11} & m_{12} & \cdots & m_{1n} \\ m_{21} & m_{22} & \cdots & m_{2n} \\ \vdots & \ddots & \ddots & \vdots \\ m_{T1} & m_{T2} & \cdots & m_{Tn} \end{pmatrix} \cdot \begin{pmatrix} a_{11} \\ a_{21} \\ \vdots \\ a_{n1} \end{pmatrix} + \begin{pmatrix} b_1 \\ b_1 \\ \vdots \\ b_1 \end{pmatrix}.\end{split}\]

Như vậy chúng ta bruteforce \(b_1\) và kiểm tra xem phương trình có nghiệm \(\begin{pmatrix} a_{11} \\ \vdots \\ a_{n1} \end{pmatrix}\) không. Nếu có thì đây là kết quả cần tìm.

Tiến hành cho từng cột của \(\bm{A}\) và từng phần tử \(b_i\) sẽ tìm được ánh xạ affine.

Unknown function

Đây là bài 8 ở round 1 và bài 10 ở round 2.

Đề bài

Bob tạo một máy lượng tử \(M\) mã hóa 4-bit word với 4-bit secret key \(k = (k_1, k_2, k_3, k_4)\) theo mạch sau:

../../_images/problem-10.png

Máy này thực hiện trên 4-bit plaintext \(x = (x_1, x_2, x_3, x_4)\) với 4-qubit tương ứng, gọi là "plainstate", là \(\lvert x_1, x_2, x_3, x_4 \rangle\), tương tự với "keystate" là \(\lvert k_1, k_2, k_3, k_4 \rangle\).

Mô hình tính toán là

\[\lvert x \rangle \lvert k \rangle \xrightarrow{M} \lvert x \rangle \lvert k \oplus f(x) \rangle,\]

trong đó

\[f(x) = (f_1(x), f_2(x), f_3(x), f_4(x))\]

là hàm Boolean vectorial khả nghịch trên \(4\) biến. Khi đó "cipherstate" là

\[\lvert k_1 \oplus f_1(x), k_2 \oplus f_2(x), k_3 \oplus f_3(x), k_4 \oplus f_4(x) \rangle\]

và cuối cùng ta thu được ciphertext.

Alice có thể khôi phục secret key \(k\) không nếu cô ấy không biết hàm \(f\)?

Giả sử Alice có oracle access tới máy lượng tử với khóa cố định \(k\) và thu được thông tin

\[f(0, 0, 0, 0) \oplus f(1, 1, 1, 1) \oplus f(1, 0, 0, 0) = c \in \mathbb{F}_2^4\]

với \(c\) đã biết.

Lời giải

Đặt \(\bm{x} = (x_1, x_2, x_3, x_4) \in \mathbb{F}_2^4\).

Kí hiệu bảng chân trị của các hàm bool \(f_1(\bm{x})\), \(f_2(\bm{x})\), \(f_3(\bm{x})\)\(f_4(\bm{x})\)

\(x_1\)

\(x_2\)

\(x_3\)

\(x_4\)

\(f_1(\bm{x})\)

\(f_2(\bm{x})\)

\(f_3(\bm{x})\)

\(f_4(\bm{x})\)

\(0\)

\(0\)

\(0\)

\(0\)

\(m_0\)

\(n_0\)

\(p_0\)

\(q_0\)

\(0\)

\(0\)

\(0\)

\(1\)

\(m_1\)

\(n_1\)

\(p_1\)

\(q_1\)

\(0\)

\(0\)

\(1\)

\(0\)

\(m_2\)

\(n_2\)

\(p_2\)

\(q_2\)

\(0\)

\(0\)

\(1\)

\(1\)

\(m_3\)

\(n_3\)

\(p_3\)

\(q_3\)

\(0\)

\(1\)

\(0\)

\(0\)

\(m_4\)

\(n_4\)

\(p_4\)

\(q_4\)

\(0\)

\(1\)

\(0\)

\(1\)

\(m_5\)

\(n_5\)

\(p_5\)

\(q_5\)

\(0\)

\(1\)

\(1\)

\(0\)

\(m_6\)

\(n_6\)

\(p_6\)

\(q_6\)

\(0\)

\(1\)

\(1\)

\(1\)

\(m_7\)

\(n_7\)

\(p_7\)

\(q_7\)

\(1\)

\(0\)

\(0\)

\(0\)

\(m_8\)

\(n_8\)

\(p_8\)

\(q_8\)

\(1\)

\(0\)

\(0\)

\(1\)

\(m_9\)

\(n_9\)

\(p_9\)

\(q_9\)

\(1\)

\(0\)

\(1\)

\(0\)

\(m_{10}\)

\(n_{10}\)

\(p_{10}\)

\(q_{10}\)

\(1\)

\(0\)

\(1\)

\(1\)

\(m_{11}\)

\(n_{11}\)

\(p_{11}\)

\(q_{11}\)

\(1\)

\(1\)

\(0\)

\(0\)

\(m_{12}\)

\(n_{12}\)

\(p_{12}\)

\(q_{12}\)

\(1\)

\(1\)

\(0\)

\(1\)

\(m_{13}\)

\(n_{13}\)

\(p_{13}\)

\(q_{13}\)

\(1\)

\(1\)

\(1\)

\(0\)

\(m_{14}\)

\(n_{14}\)

\(p_{14}\)

\(q_{14}\)

\(1\)

\(1\)

\(1\)

\(1\)

\(m_{15}\)

\(n_{15}\)

\(p_{15}\)

\(q_{15}\)

Ở đây \(m_i\) là giá trị của hàm \(f_1(\bm{x})\) tại dòng thứ \(i\) với \(0 \leqslant i \leqslant 15\). Tương tự cho \(n_i\), \(p_i\)\(q_i\).

Ta đã biết

\[f(0, 0, 0, 0) \oplus f(1, 1, 1, 1) \oplus f(1, 0, 0, 0) = \bm{c} \in \mathbb{F}_2^4\]

với \(\bm{c}\) đã biết.

Như vậy ta có

\[\begin{split}& f(0, 0, 0, 0) = (m_0, n_0, p_0, q_0), \\ & f(1, 1, 1, 1) = (m_{15}, n_{15}, p_{15}, q_{15}), \\ & f(1, 0, 0, 0) = (m_8, n_8, p_8, q_8),\end{split}\]

suy ra

\[\bm{c} = (m_0 \oplus m_{15} \oplus m_8, n_0 \oplus n_{15} \oplus n_8, p_0 \oplus p_{15} \oplus p_8, q_0 \oplus q_{15} \oplus q_8).\]

"Cipherstate"

\[\lvert k_1 \oplus f_1(\bm{x}), k_2 \oplus f_2(\bm{x}), k_3 \oplus f_3(\bm{x}), k_4 \oplus f_4(\bm{x}) \rangle\]

chính là \(4\) bit ciphertext

\[(k_1 \oplus f_1(\bm{x}), k_2 \oplus f_2(\bm{x}), k_3 \oplus f_3(\bm{x}), k_4 \oplus f_4(\bm{x})).\]

Vì Alice có truy cập oracle tới máy với khóa cố định \(k\) nên

  1. Nếu Alice gửi plaintext \((0, 0, 0, 0)\) thì sẽ nhận lại ciphertext

\[\bm{C}_0 = (k_1 \oplus m_0, k_2 \oplus n_0, k_3 \oplus p_0, k_4 \oplus q_0).\]
  1. Nếu Alice gửi plaintext \((1, 1, 1, 1)\) thì sẽ nhận lại ciphertext

\[\bm{C}_{15} = (k_1 \oplus m_{15}, k_2 \oplus n_{15}, k_3 \oplus p_{15}, k_4 \oplus q_{15}).\]
  1. Nếu Alice gửi plaintext \((1, 0, 0, 0)\) thì sẽ nhận lại ciphertext

\[\bm{C}_8 = (k_1 \oplus m_8, k_2 \oplus n_8, k_3 \oplus p_8, k_4 \oplus q_8).\]

Nói cách khác là

\[\begin{split}\bm{C}_0 \oplus \bm{C}_{15} \oplus \bm{C}_8 & = (k_1 \oplus (m_0 \oplus m_{15} \oplus m_8), k_2 \oplus (n_0 \oplus n_{15} \oplus n_8), k_3 \oplus (p_0 \oplus p_{15} \oplus p_8), k_4 \oplus (q_0 \oplus q_{15} \oplus q_8)) \\ & = (k_1, k_2, k_3, k_4) \oplus \bm{c}.\end{split}\]

Ta đã biết vector \(\bm{c}\), và có thể tính \(\bm{C}_0 \oplus \bm{C}_{15} \oplus \bm{C}_8\) nên hoàn toàn có thể tìm lại được khóa \(\bm{k}\) mà không cần hàm \(f\).

A simple hash function

Đây là bài 11 ở round 2.

Đề bài

Carol tạo một thuật toán hash mới. Khóa \(k = (k_1, \ldots, k_6)\) là vector nhị phân độ dài \(6\).

Đầu vào hàm hash là một dãy các chữ số. Dãy này được chia thành các khối độ dài \(6\). Nếu độ dài của dãy không chia hết cho sau thì ta thêm lần lượt các chữ số \(1\), \(2\), \(3\), ... vào sau dãy cho đến khi độ dài của dãy chia hết cho \(6\).

Mỗi khối, giả sử là \((p_1, \ldots, p_6)\), được biến đổi thành số nguyên theo quy tắc

\[(-1)^{k_1} \cdot p_1 + \ldots + \ldots + (-1)^{k_6} \cdot p_6.\]

Ở đây \((-1)^0 = 1\)\((-1)^1 = -1\).

Kết quả sau khi tính toán các khối \(n_1\), \(n_2\), ... sau đó được tổng hợp thành giá trị hash là

\[H = n_1 - n_2 + n_3 - n_4 + \cdots\]

Carol áp dụng thuật toán hash này vào hệ thống log của trang web ngân hàng. Mỗi khi người dùng nhập mật khẩu \(P\) thì hệ thống sẽ tính toán hash \(H(P, K)\) và so sánh với giá trị hash lưu trên cơ sở dữ liệu để xem người dùng có nhập đúng mật khẩu không.

Nhưng mà Carol nhận ra hệ thống này không an toàn. Việc tìm va chạm (collision) là có thể và từ đó kẻ tấn công có thể truy cập hệ thống. Họ làm như thế nào?

Hãy đề xuất thuật toán đơn giản nhất để tìm va chạm cho mọi dãy đầu vào \(P\) biết trước nếu khóa \(K\) không biết.

Hơn nữa hãy tìm va chạm ngắn nhất cho dãy trong ví dụ (dãy \(P = 134875512293\)). Lưu ý rằng do không biết \(K\) nên chúng ta cũng không biết giá trị hash \(H(P, K)\).

Lời giải

Bài này mình có chút nhầm lẫn nên không được full điểm.

Giả sử input có \(t\) khối là \(n_1\), \(n_2\), ..., \(n_t\). Mỗi khối có \(6\) chữ số.

Mình sẽ chứng minh rằng khi \(t\) chẵn thì ta luôn tìm được collision mà không cần khóa \(K\).

Đầu tiên, với \(t = 2\), giả sử hai khối là

\[n_1 = (p_{1, 1}, \ldots, p_{1, 6}), \quad n_2 = (p_{2, 1}, \ldots, p_{2, 6}).\]

Giả sử khóa là \(K = (k_1, \ldots, k_6)\). Khi đó hash là

\[\begin{split}H & = (-1)^{k_1} \cdot p_{1, 1} + \ldots + (-1)^{k_6} \cdot p_{1, 6} \\ & - \left[ (-1)^{k_1} \cdot p_{2, 1} + \ldots + (-1)^{k_6} \cdot p_{2, 6} \right]\end{split}\]

và nếu mình tăng hoặc giảm \(p_{1, i}\)\(p_{2, i}\), với \(1 \leqslant i \leqslant 6\), cùng một lượng, ví dụ như

\[p_{1, 1}' = p_{1, 1} + a, \quad p_{2, 1}' = p_{2, 1} + a,\]

và thay \(p_{1, 1}'\)\(p_{2, 1}'\) vào hash thì được

\[\begin{split}H' & = (-1)^{k_1} \cdot p_{1, 1}' + \ldots + (-1)^{k_6} \cdot p_{1, 6} - \left[ (-1)^{k_1} \cdot p_{2, 1}' + \ldots + (-1)^{k_6} \cdot p_{2, 6} \right] \\ & = (-1)^{k_1} \cdot (p_{1, 1} + a) + \ldots + (-1)^{k_6} \cdot p_{1, 6} - \left[ (-1)^{k-1} \cdot (p_{2, 1} + a) + \ldots + (-1)^{k_6} \cdot p_{2, 6} \right] \\ & = H + (-1)^{k_1} \cdot a - \left[ (-1)^{k_1} \cdot a \right] = H.\end{split}\]

Điều đó nghĩa là nếu tăng hoặc giảm \(p_{1, i}\)\(p_{2, i}\) cùng lượng, tức là

\[p_{1, i}' = p_{1, i} + a_i, \ p_{2, i}' = p_{2, i} + a_i, \ \text{where} \ a_i \in \{ -1, 0, 1\},\]

và phải thỏa điều kiện \(1 \leqslant p_{1, i}' \leqslant 9\)\(1 \leqslant p_{2, i}' \leqslant 9\) thì mình sẽ tìm được input mới có cùng hash.

Khi \(t = 4, 6, \ldots\) thì sử dụng phương pháp tương tự cho các cặp khối \(n_{2m+1}\)\(n_{2m+2}\) (mỗi cặp sử dụng các giá trị \(a_i\) tùy ý miễn thỏa các điều kiện trên) thì ta luôn tìm được collision bất kể khóa \(K\) là gì.

Cryptographic Fish

Đây là bài 1 ở round 1.

../../_images/problem-1-round-1.png

Đây là tam giác Pascal.

Mình đã có một hàng là

\[1 \to 3 \to 3 \to 1.\]

Hàng kế tiếp là

\[1 \to 4 \to {\color{red}6} \to 4 \to 1.\]

Hàng kế nữa là

\[1 \to {\color{red}5} \to 10 \to 20 \to 10 \to {\color{red}5} \to 1.\]

Tiếp theo sẽ là

\[1 \to 6 \to {\color{red}15} \to {\color{red}15} \to 6 \to 1\]

vì theo thứ tự chúng ta sẽ cần \(C^2_6\) (đi từ trên xuống) và \(C^4_6\) (đi từ dưới lên).

Hàng cuối sẽ là

\[1 \to 7 \to 21 \to {\color{red}35} \to {\color{red}35} \to 21 \to 7 \to 1.\]

Còn về ô đầu tiên thì cần điền số \(\color{red}2\) do dòng trước \(3\)\(2\) theo tam giác Pascal.

Như vậy kết quả là

\[2 + 6 + 5 + 5 + 15 + 15 + 35 + 35 = 118.\]