206 lines
6.2 KiB
Python
206 lines
6.2 KiB
Python
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}") |