Part of the answer is that the two questions in the question
are in fact different questions.
The ring $\mathbb{Z}/2^8\mathbb{Z}$ of integers modulo $2^8$ is
a very different ring from the field $F_{2^8}$ with $2^8$ elements.
In particular it is not a field and not even an integral domain.
In Sage these two rings are Zmod(2^8)
and GF(2^8)
.
Pursing that line of thought, given a linear system involving
integers between 0 and 255, one can consider it as a system
to be solved over $\mathbb{R}$, over $\mathbb{R}$,
over $\mathbb{Z}/2^8\mathbb{Z}$, or over the finite field
with $2^8$ elements, considering the integers as encodings
of field elements.
In this answer we address solving modulo $2^8$
and solving in the finite field.
Solve a linear system modulo 256
Let's see how to define the system, find one solution,
find all solutions.
The system
Define the ring $\mathbb{Z}/2^8\mathbb{Z}$,
the matrix, the target vector:
sage: Z = Zmod(2^8)
sage: A = matrix(Z, 3, [[23, 56, 2],
....: [92, 234, 182],
....: [12, 94, 132]])
sage: y = vector(Z, 3, [141, 40, 10])
Check:
sage: A
[ 23 56 2]
[ 92 234 182]
[ 12 94 132]
sage: y
(141, 40, 10)
Find one solution
To find one solution to the system, i.e. a preimage of the
target vector under the matrix, use the solve_right
method:
sage: x = A.solve_right(y)
sage: x
(13, 83, 157)
Check:
sage: yy = A * x
sage: yy
(141, 40, 10)
sage: yy == y
True
Find all solutions
Finding all solutions is now equivalent to finding the right kernel
of $A$ i.e. all vectors $x$ such that $A x$ is zero.
Sadly, finding that kernel is not implemented in Sage:
sage: A.right_kernel()
Traceback (most recent call last)
...
NotImplementedError: Echelon form not implemented
over 'Ring of integers modulo 256'.
The brute force solution is to try all vectors.
There are $(2^8)^3$, i.e. $2^{24}$, i.e. 16,777,216 of them.
Testing them all would take a while and waste resources.
Instead, we can observe that if $A x$ is zero in
$\mathbb{Z}/2^8\mathbb{Z}$, then it is also zero
modulo $\mathbb{Z}/2^k\mathbb{Z}$
for $k \in \{7, 6, 5, 4, 3, 2, 1, 0\}$.
For each of these $k$, call $A_k$ the matrix $A$
projected to $\mathbb{Z}/2^k\mathbb{Z}$.
The level $k = 0$ only has the zero matrix and the zero vector,
so the kernel there is everything.
For the kernel of the projected matrix $A_1$, we can try all
8 vectors in $\mathbb{Z}/2^1\mathbb{Z}$, we find that 4 of them
are part of the kernel.
For each $k$, each vector in the kernel of $A_{k-1}$
has $2^3$ lifts to $(\mathbb{Z}/2^k\mathbb{Z})^3$,
which are the only candidates for forming the kernel of $A_k$.
For each starting vector in $\ker(A_1)$, we thus lift progressively,
at each step there are eight vectors to try but only one of them
ends up being in the kernel at the next level.
We end up computing 8 matrix times vector products in the first
level (or computing a kernel basis by finding the row reduced
echelon form over $\mathbb{Z}/2\mathbb{Z}$ which is a field,
and then, for each of the 4 vectors of the level 1 kernel,
8 matrix times vector products in each of the next 7 levels.
In total, that means $8 + 4 \times 7 \times 8$, i.e. $225$ matrix
times vector products. That's a lot more reasonable than the
roughly 16.8 million matrix times vector products that brute force
search would cost.
This function computes the kernel following that method:
def right_kernel_mod_p_to_the_k(A, p=None, k=None):
r"""
Return the kernel of ``A`` modulo ``p^k``
"""
if p is None or k is None:
Z = A.base_ring()
f = Z.cardinality().factor()
if len(f) == 1:
p, k = f[0]
else:
raise ValueError("either specify p and k or use matrix with "
"base ring some p^k with p prime and k > 0")
n = A.nrows()
W = (Zmod(p)^n).list()
K = A.change_ring(Zmod(p)).right_kernel().list()
for j in range(2, k + 1):
R = Zmod(p^j)
V = R^n
B = A.change_ring(R)
W = [p * V(v) for v in W]
K = [V(x) for x in K]
# candidates
C = (u + v for u in K for v in W)
K = [x for x in C if (B*x).is_zero()]
return K
Compute the kernel with that function:
sage: K = right_kernel_mod_p_to_the_k(A)
sage: K
[(0, 0, 0), (0, 128, 0), (0, 0, 128), (0, 128, 128)]
Deduce the solutions to the original system:
sage: S = [x + u for u in K]
sage: S
[(13, 83, 157), (13, 211, 157), (13, 83, 29), (13, 211, 29)]
Check:
sage: [A*x for x in S]
[(141, 40, 10), (141, 40, 10), (141, 40, 10), (141, 40, 10)]
Solve a linear system over the finite field with 256 elements
This is a different problem. As given, it amounts to
solving a linear system in $F_{2^8}$, with the extra steps
of converting field elements from integer representation
to polynomial representation and back.
A related question and answer are at
Given the severeal steps, we write a function:
def preimage(y, A):
r"""
Return a vector `x` over `F_{2^8}` such that `A x = y`.
Here `F_{2^8}` is the finite field with `2^8` elements
with minimal weight modulus.
INPUT:
- ``y`` -- a vector with entries in `F_{2^8}`
where each entry is represented as an integer
- ``A`` -- a matrix with entries in `F_{2^8}`
where each entry is represented as an integer
OUTPUT:
A vector ``x`` with entries in `F_{2^8}` such that `A x = y`.
Each entry is represented as an integer.
"""
F = GF(2^8, 'a', modulus='minimal_weight')
n = A.nrows()
AA = matrix(F, n, n, lambda i, j: F.fetch_int(A[i, j]))
yy = vector(F, n, [F.fetch_int(c) for c in y])
xx = AA.solve_right(yy)
return vector(ZZ, n, [c.integer_representation() for c in xx])
Example:
sage: A = matrix(ZZ, 3, [[23, 56, 2],
....: [92, 234, 182],
....: [12, 94, 132]])
sage: y = vector(ZZ, 3, [141, 40, 10])
sage: x = preimage(y, A)
sage: x
(194, 215, 241)
Here, finding the kernel would be easier, since the base ring
of the relevant matrix is a field.
F.<a> = GF(2^8, modulus="minimal_weight")
? Other than understanding that the representation is wrong, how do I (correctly) do what I intended to do? – milck Apr 04 '21 at 07:03