def check_affine_equivalence(f, g, A, a, B, b): """Checks whether f(x) = (B o g o A)(x + a) + b""" for x in xrange(0, 2**N): y = oplus(x, a) y = apply_bin_mat(y, A) y = g[y] y = apply_bin_mat(y, B) y = oplus(y, b) if y != f[x]: return False return True
def affine_equivalence(f, g): """Returns, if it exists, the tuple A, a, B, b where A and B are matrices and where a and b are integers such that, for all x: f(x) = (B o g o A)(x + a) + b, where "o" denotes functional composition and "+" denotes XOR. If no such affine permutations exist, returns an empty list. Internally calls a function written in C++ for speed which returns the "Linear Representative" using an algorithm from Alex Biryukov, Christophe De Canniere, An Braeken, and Bart Preneel (2003). "A Toolbox for Cryptanalysis: Linear and Affine Equivalence Algorithms", Advances in Cryptology -- EUROCRYPT 2003, Lecture Notes in Computer Science 2656, E. Biham (ed.), Springer-Verlag, pp. 33--50, 2003. """ if len(f) != len(g): raise "f and g are of different dimensions!" table_f = defaultdict(list) table_c = defaultdict(int) for c in xrange(0, len(f)): f_c = le_class_representative([oplus(f[x], c) for x in xrange(0, len(f))]) d = hash_sbox(f_c) table_f[d] = f_c table_c[d] = c rs = [] a = -1 b = -1 for c in xrange(0, len(f)): g_c = le_class_representative([g[oplus(x, c)] for x in xrange(0, len(f))]) d = hash_sbox(g_c) if d in table_c.keys(): a = c b = table_c[d] rs = g_c break if a == -1: return [] l_f = linear_equivalence([oplus(f[x], b) for x in xrange(0, len(f))], rs) A_f, B_f = l_f[0], l_f[1] l_g = linear_equivalence([g[oplus(x, a)] for x in xrange(0, len(f))], rs) A_g, B_g = l_g[0], l_g[1] A = A_g.inverse() * A_f B = B_f * B_g.inverse() a = apply_bin_mat(a, A.inverse()) return [A, a, B, b]
def test_ae_equivalence(N, verbose=False): from timeit import default_timer false_negatives = 0 false_positives = 0 n_tested = 10 for i in xrange(0, n_tested): # checking if linearly equivalent permutations are identified f = random_permutation(N) A = rand_linear_permutation(N) a = randint(0, 2**N-1) B = rand_linear_permutation(N) b = randint(0, 2**N-1) g = [oplus(apply_bin_mat(f[apply_bin_mat(oplus(x, a), A)], B), b) for x in xrange(0, 2**N)] computation_start = default_timer() result = affine_equivalence(f, g) computation_end = default_timer() elapsed = computation_end - computation_start if len(result) > 1: if not check_affine_equivalence(f, g, result[0], result[1], result[2], result[3]): false_negatives += 1 if verbose: print "[FAIL] wrong affine permutations" else: if verbose: print "[success] AE {:0.4f}".format(elapsed) else: false_negatives += 1 if verbose: print "[FAIL] AE {:0.4f}s (nothing found)".format(elapsed) # checking if non-affine equivalent functions are identified g = random_permutation(N) result = affine_equivalence(f, g) if len(result) == 0: if verbose: print "[success] non-AE {:0.4f}s".format(elapsed) else: if check_affine_equivalence(f, g, result[0], result[1], result[2], result[3]): if verbose: print "[success] act.AE {:0.4f}".format(elapsed) else: false_positives += 1 if verbose: "[FAIL] matrices found for non-LE permutations" print "* testing if AE functions are identified correctly (with correct affine permutations)" print_result(n_tested-false_negatives, n_tested) print "* testing if NON-LE functions are identified correctly" print_result(n_tested-false_positives, n_tested)
def xor_equivalence(f, g): """Returns a pair [k0, k1] of integers such that, for all x: f[x] = g[x + k0] + k1, where "+" denotes the bitwise XOR. """ N = int(log(len(f), 2)) for k0 in xrange(0, 2**N): k1 = oplus(f[0], g[k0]) good = True for x in xrange(1, 2**N): if oplus(f[x], g[oplus(k0, x)]) != k1: good = False break if good: return [k0, k1] return []
def __call__(self, x): """Returns the result of applying L to the integer x, intepreting it as a binary vector. """ result = 0 for i in xrange(0, len(self.masks)): if (x >> i) & 1 == 1: result = oplus(result, self.masks[i]) return result
def partial_linear_permutation_to_full(v, n): """Returns a matrix corresponding to a linear permutation of n bits mapping any x < len(v) to v[x]. """ if len(v) > 2**n: raise "n should be at least as big as the dimension of the state" if v[0] != 0: raise "v should start with 0" u = int(log(len(v), 2)) if len(v) != 2**u: raise "size of v should be a power of 2" basis = [] rebuilt_space = [0] # finding a basis of v for new_vector in v: if new_vector not in rebuilt_space: basis.append(new_vector) new_rebuilt_space = list(rebuilt_space) for x in rebuilt_space: new_rebuilt_space.append(oplus(x, new_vector)) rebuilt_space = new_rebuilt_space if len(basis) == u: break # completing the basis while len(rebuilt_space) < 2**n: new_vector = 0 while new_vector in rebuilt_space: new_vector += 1 basis.append(new_vector) new_rebuilt_space = list(rebuilt_space) for x in rebuilt_space: new_rebuilt_space.append(oplus(x, new_vector)) rebuilt_space = new_rebuilt_space result = Matrix(GF(2), n, n, [[(b >> (n - 1 - i)) & 1 for i in xrange(0, n)] for b in reversed(basis)]).transpose() check = linear_function_matrix_to_lut(result) if check[0:len(v)] == v: return result else: raise "no such matrix"
def linear_span(basis, with_zero=True): result = [] if with_zero: result.append(0) visited = defaultdict(int) visited[0] = 1 for i in xrange(1, 2**len(basis)): x = 0 for j in xrange(0, len(basis)): if (i >> j) & 1 == 1: x = oplus(x, basis[j]) if visited[x] != 1: result.append(x) visited[x] = 1 return result
def extract_direct_sum(list_of_spaces, N): space_pairs = [] for va, vb in itertools.combinations(list_of_spaces, 2): # basic tests good = True if len(va[0]) * len(vb[0]) < 2**N: good = False if good: if len(va[1]) * len(vb[1]) < 2**N: good = False if good: if rank_of_vector_set(va[0] + vb[0], N) != N: good = False if good: if rank_of_vector_set(va[1] + vb[1], N) != N: good = False if good: # basic tests passed # more complicated cases # -- the y coordinate is good but the x coordinate is too big if len(va[1]) * len( vb[1]) == 2**N and len(va[0]) * len(vb[0]) > 2**N: if len(vb[0]) > len(va[0]): # case where vb needs shringking # print "1", va[0], vb[0] new_vb0_indicator = defaultdict(int) for x in vb[0]: new_vb0_indicator[x] = 1 for v in va[0]: if v != 0: for x in vb[0]: new_vb0_indicator[oplus(x, v)] = 0 new_b0 = [ x for x in new_vb0_indicator.keys() if new_vb0_indicator[x] == 1 ] vb[0] = new_b0 elif len(va[0]) > len(vb[0]): # case where va needs shringking # print "2" new_va0_indicator = defaultdict(int) for x in va[0]: new_va0_indicator[x] = 1 for v in vb[0]: if v != 0: for x in va[0]: new_va0_indicator[oplus(x, v)] = 0 new_a0 = [ x for x in new_va0_indicator.keys() if new_va0_indicator[x] == 1 ] va[0] = new_a0 # -- the x coordinate is good but the y coordinate is too big if len(va[0]) * len( vb[0]) == 2**N and len(va[1]) * len(vb[1]) > 2**N: if len(vb[1]) > len(va[1]): # case where vb needs shringking # print "3" new_vb1_indicator = defaultdict(int) for x in vb[1]: new_vb1_indicator[x] = 1 for v in va[1]: if v != 0: for x in vb[1]: new_vb1_indicator[oplus(x, v)] = 0 new_b1 = [ x for x in new_vb1_indicator.keys() if new_vb1_indicator[x] == 1 ] vb[1] = new_b1 elif len(va[1]) > len(vb[1]): # case where va needs shringking # print "4" new_va1_indicator = defaultdict(int) for x in va[1]: new_va1_indicator[x] = 1 for v in vb[1]: if v != 0: for x in va[1]: new_va1_indicator[oplus(x, v)] = 0 new_a1 = [ x for x in new_va1_indicator.keys() if new_va1_indicator[x] == 1 ] va[1] = new_a1 # simple case: all spaces are just the right size if len(va[0]) * len(vb[0]) == 2**N and len(va[1]) * len( vb[1]) == 2**N: space_pairs.append([va, vb]) return space_pairs
def extract_affine_bases(z, dimension, word_length, n_threads=DEFAULT_N_THREADS, number="fixed dimension"): """Returns a list containing the Gaussian Jacobi basis of each affine space of dimension `dimension` that is contained in the list `z` of integers intepreted as elements of $\F_2^n$ where $n$ is equal to `word_length`. The number of threads to use can be specified using the argument `n_threads`. It can have 3 different behaviours depending on the value of the argument `number`: - if it is "just one" then it will stop as soon as it has found an affine space of the desired dimension and return its basis; - if it is "fixed dimension" then it will return all affine spaces with exactly the given dimension, even if they are affine subspaces of a larger space contained in `z`; and - if it is "all dimensions" then it will return all vector spaces of dimensions at least `dimension`. If a larger vector space is found, its bases will be return and its subspaces will be ignored. """ if number not in ["all dimensions", "fixed dimension", "just one"]: raise Exception( "Unknown value for parameter `number` in extract_affine_bases:" + number) result = extract_affine_bases_fast(z, int(dimension), int(word_length), int(n_threads), str(number)) if number == "all dimensions": # in the case where we have obtained larger spaces, we remove # their subspaces from the list bigger_affine = [[oplus(b[0], x) for x in linear_span(b[1:])] for b in result if len(b) > dimension + 1] if len(bigger_affine) == 0: # nothing to do as there is no bigger space return result else: new_result = list([b for b in result if len(b) > dimension + 1]) for b in result: if len(b) == dimension + 1: aff = [oplus(b[0], v) for v in linear_span(b[1:])] is_included_in_bigger = False for big_space in bigger_affine: is_included = True for v in aff: if v not in big_space: is_included = False break if is_included: is_included_in_bigger = True break if not is_included_in_bigger: new_result.append(b) return new_result else: return result