import json import numpy as np # --- 1. Setup Parameters --- q = 251 n = 16 k = 2 # --- 2. Helper Functions --- def negacyclic_matrix(poly_coeffs): """Converts a polynomial to a negacyclic matrix.""" mat = [] # In the challenge, polynomial mult involves convolution and subtraction # This corresponds to standard negacyclic structure for x^n + 1 p = list(poly_coeffs) for i in range(n): row = [0] * n for j in range(n): if i - j >= 0: row[j] = p[i - j] else: row[j] = -p[i - j + n] # Negate wrap-around mat.append(row) # The challenge uses numpy convolve which results in a specific layout. # Let's verify orientation. # v0 * v1 in challenge is sum(convolve(a,b)...) # We want Matrix * Vector = Result. # The matrix constructed above represents multiplication BY poly_coeffs return Matrix(ZZ, mat) def get_secret_key(A_polys, t_polys): # Construct the large A matrix (Block matrix) # A has shape 2x2. Each element is a polynomial. # We convert each polynomial to a 16x16 matrix. # A_big will be 32x32. # A_polys structure: [[poly00, poly01], [poly10, poly11]] M00 = negacyclic_matrix(A_polys[0][0]) M01 = negacyclic_matrix(A_polys[0][1]) M10 = negacyclic_matrix(A_polys[1][0]) M11 = negacyclic_matrix(A_polys[1][1]) # Block matrix construction A_big = block_matrix([[M00, M01], [M10, M11]]) # Flatten t into a single vector of size 32 t_vec = vector(ZZ, A_polys[0][0]).parent().zero_vector() # dummy init t_flat = [] for row in t_polys: t_flat.extend(row) t_vec = vector(ZZ, t_flat) # --- Lattice Embedding --- # We want to solve s * A_big = t (approx) # Note: In the challenge code: t = A * s + e # So A_big * s_vec = t_vec (mod q) # Lattice Basis Construction (Kannad Embedding) # Rows: # [ I_32 | A_big^T | 0 ] # [ 0 | q*I_32 | 0 ] # [ 0 | -t | 1 ] dim = 2 * n # Identity B0 = identity_matrix(dim) # A Transpose (because A*s = t, we put columns of A into rows of lattice basis part) # Wait, if we use row vectors v*M, we need appropriate orientation. # Let's stick to: we want vector (s, e, 1) # A_big * s - t = -e (mod q) # => A_big * s + q*k - t = -e L = matrix(ZZ, 2*dim + 1, 2*dim + 1) # Top Left: Identity for s L.set_block(0, 0, identity_matrix(dim)) # Top Middle: A_big Transpose (mapping s to A*s) # We use Transpose because Sage lattices are row-span. # A row (s) * (A^T) = (A*s)^T L.set_block(0, dim, A_big.transpose()) # Middle Middle: q * Identity (modulus reduction) L.set_block(dim, dim, q * identity_matrix(dim)) # Bottom Middle: -t vector L.set_block(2*dim, dim, matrix(ZZ, 1, dim, [-x for x in t_flat])) # Bottom Right: 1 (constant for embedding) L[2*dim, 2*dim] = 1 print("Running LLL...") L_reduced = L.LLL() # Search for the short vector. It should look like (s, -e, 1) or -(s, -e, 1) for row in L_reduced: if row[2*dim] == 1: # Check the embedding constant s_recovered = row[:dim] print("Found candidate secret key!") return s_recovered elif row[2*dim] == -1: s_recovered = [-x for x in row[:dim]] print("Found candidate secret key (inverted)!") return s_recovered return None # --- 3. Decryption Routine --- def poly_mul_mod(p1, p2): """Re-implementation of the challenge multiplication for decryption""" res = [0] * (2 * n) for i in range(n): for j in range(n): res[i+j] += p1[i] * p2[j] # Reduction mod x^16 + 1 out = [0] * n for i in range(len(res)): if i < n: out[i] = (out[i] + res[i]) else: out[i - n] = (out[i - n] - res[i]) # x^16 = -1 return [x % q for x in out] def decrypt(u, v, s): # u is a list of lists (k x n) # v is a list (n) # s is the recovered secret (k x n flattened) # Reconstruct s into polys s_polys = [list(s[i*n : (i+1)*n]) for i in range(k)] # Calculate s^T * u # u is shape (k, n). s is shape (k, n). # Dot product = sum(poly_mul(s[i], u[i])) dot_prod = [0]*n for i in range(k): prod = poly_mul_mod(s_polys[i], u[i]) dot_prod = [(x + y) % q for x, y in zip(dot_prod, prod)] # m_noisy = v - s*u m_noisy = [(v_val - d_val) % q for v_val, d_val in zip(v, dot_prod)] # Rounding # Center is roughly q/2 = 126. # If close to 0 -> 0. If close to 126 -> 1. bits = [] limit = q // 4 for val in m_noisy: # Distance to 0 d0 = min(val, q - val) # Distance to q/2 target = (q + 1) // 2 d1 = min(abs(val - target), q - abs(val - target)) if d1 < d0: bits.append(1) else: bits.append(0) return bits # --- 4. Main Execution --- # Load Data with open("keys.json", "r") as f: data = json.load(f) A = data['A'] t = data['t'] u_list = data['u'] v_list = data['v'] # Recover S s_vec = get_secret_key(A, t) # Decrypt all batches all_bits = [] for u_batch, v_batch in zip(u_list, v_list): # u_batch is shape (k, n). v_batch is shape (n) (actually it's list of lists in json?) # Wait, check JSON structure. # v is list of lists. u is list of lists of lists. # But decrypt function inputs: u_list is list of u. v_list is list of v. # In encrypt: u_list.append(u), u is numpy array. # Let's handle the specific JSON structure provided in the prompt # u in json is [ [row1, row2], [row1, row2] ... ] # v in json is [ poly, poly ... ] decrypted_bits = decrypt(u_batch, v_batch, s_vec) all_bits.extend(decrypted_bits) # --- 5. Bits to Flag --- # The challenge code: [int(c) for char in flag for c in format(ord(char), '08b')] # This groups 8 bits per char. flag = "" for i in range(0, len(all_bits), 8): byte = all_bits[i:i+8] if len(byte) < 8: break # Convert list of 0/1 to integer char_code = int("".join(map(str, byte)), 2) flag += chr(char_code) print(f"Flag: {flag}")