Files
ctf/2025/lake/crypto/sage_shit/solve.sage
2025-12-15 20:03:09 +01:00

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}")