Module cryptanalysis.characteristic_searcher
characteristic_searcher
This module provides the CharacteristicSearcher class for performing characteristic search in cryptanalysis.
Classes: - CharacteristicSearcher: Implements the search algorithm for finding linear and differential characteristics in substitution permutation network based cipher.
Expand source code
"""
characteristic_searcher
===========
This module provides the CharacteristicSearcher class for performing characteristic search in cryptanalysis.
Classes:
- CharacteristicSearcher: Implements the search algorithm for finding linear and differential characteristics in substitution permutation network based cipher.
"""
from collections import defaultdict
from math import log2
from itertools import islice
from z3 import Optimize, BitVec, sat, Concat, And, Not, Implies, Extract, Function, BitVecSort, RealSort, Product
from .utils import calculate_linear_bias, calculate_difference_table, all_smt
__all__ = ["CharacteristicSearcher"]
class CharacteristicSearcher:
"""A class for finding characteristics (linear or differential) of a substitution
permutation network with provided S-box and P-box with a given number of rounds.
Attributes:
sbox: A list representing the substitution box.
pbox: A list representing the permutation box.
num_rounds: An integer representing the number of rounds.
block_size: An integer representing the number of bits in the block.
box_size: An integer representing the size of the S-box in bits.
num_blocks: An integer representing the number of sboxes in a block
mode: A string representing the mode, which can be 'linear' or 'differential'.
bias: A Counter dictionary representing linear or differential bias
of sbox input/output pairs
solutions: A dictionary containing list of valid characteristic masks for a given
set of included and excluded blocks
solver: SMT solver (optimize) instance to search the characteristics
"""
def __init__(self, sbox, pbox, num_rounds, mode='linear'):
"""Initializes the CharacteristicSolver with the given sbox, pbox, num_rounds and mode.
Args:
sbox (list): The substitution box.
pbox (list): The permutation box.
num_rounds (int): The number of rounds.
mode (str, optional): The mode of operation. Defaults to 'linear'.
"""
self.sbox = sbox
self.pbox = pbox
self.num_rounds = num_rounds
self.block_size = len(pbox)
self.box_size = int(log2(len(sbox)))
self.num_blocks = len(pbox) // self.box_size
self.mode = mode
if mode == 'linear':
self.bias = calculate_linear_bias(sbox)
elif mode == 'differential':
self.bias = calculate_difference_table(sbox)
self.solutions = defaultdict(list)
self.solver = Optimize()
self.prune_level = 0
self.sboxf = None
self.inps = None
self.oups = None
self.bv_inp_masks = None
self.bv_oup_masks = None
self.objectives = None
def initialize_sbox_structure(self):
"""Initializes the S-box structure for the cryptographic solver.
This method sets up the structure of the S-box by creating an optimized solver,
initializing input and output bit vectors for each round, and adding
constraints for the solver. It also creates a concatenated view of the input
and output layers for further processing.
"""
n = self.box_size
self.inps = [[BitVec('r{}_i{}'.format(r, i), n) for i in range(
self.num_blocks)] for r in range(self.num_rounds + 1)]
self.oups = [[BitVec('r{}_o{}'.format(r, i), n) for i in range(
self.num_blocks)] for r in range(self.num_rounds)]
# permutation of output of sboxes are inputs of next round
for i in range(self.num_rounds):
if self.num_blocks == 1:
self.solver.add(self.bitvec_permutation(
self.oups[i][0], self.inps[i + 1][0]))
else:
self.solver.add(self.bitvec_permutation(
Concat(self.oups[i]), Concat(self.inps[i + 1])))
# all first layer inputs should not be 0
self.solver.add(
Not(And(*[self.inps[0][i] == 0 for i in range(self.num_blocks)])))
for r in range(self.num_rounds):
for i in range(self.num_blocks):
# if sbox has input, it should have output
self.solver.add(
Implies(
self.inps[r][i] != 0,
self.oups[r][i] != 0))
# if sbox has no input it should not have any output
self.solver.add(
Implies(
self.inps[r][i] == 0,
self.oups[r][i] == 0))
# just a concatanated view of the input and output layers
if self.num_blocks == 1:
self.bv_inp_masks = [self.inps[i][0]
for i in range(self.num_rounds + 1)]
self.bv_oup_masks = [self.oups[i][0]
for i in range(self.num_rounds)]
else:
self.bv_inp_masks = [Concat(self.inps[i])
for i in range(self.num_rounds + 1)]
self.bv_oup_masks = [Concat(self.oups[i])
for i in range(self.num_rounds)]
def bitvec_permutation(self, inp, oup):
"""Performs bit vector permutation based on pbox.
Args:
inp (BitVec): The input bit vector.
oup (BitVec): The output bit vector.
Returns:
list: A list of constraints for the permutation.
"""
pn = len(self.pbox)
constraints = []
for i, v in enumerate(self.pbox):
constraints.append(
Extract(pn - 1 - i, pn - 1 - i, inp) ==
Extract(pn - 1 - v, pn - 1 - v, oup)
)
return constraints
def initialize_objectives(self):
"""Initializes the objective functions for the cryptographic solver.
The method sets up four types of objective functions: 'original_linear',
'reduced', 'differential', and 'linear'. These objective functions are
used to guide the solver in finding the optimal solution. Each objective
function is associated with a lambda function that calculates the objective
value for a given number of rounds.
'reduced' objective is called for both linear and differential search
Other objective functions are just there for reference and easy evaluation
of bias directly from the model
"""
self.objectives = {
# the actual objective, which is just product of bias [0,1/2]
'original_linear': lambda rounds: 2**(self.num_blocks * rounds - 1) * Product([self.sboxf(
self.inps[i // self.num_blocks][i % self.num_blocks],
self.oups[i // self.num_blocks][i % self.num_blocks])
for i in range(self.num_blocks * rounds)
]),
# reducing optimization problem of product to sums
'reduced': lambda rounds: sum([
self.sboxf(
self.inps[i // self.num_blocks][i % self.num_blocks],
self.oups[i // self.num_blocks][i % self.num_blocks])
for i in range(self.num_blocks * rounds)
]),
# objective when the input biases are [0,2**n] just the final
# division
'differential': lambda rounds: Product([
self.sboxf(
self.inps[i // self.num_blocks][i % self.num_blocks],
self.oups[i // self.num_blocks][i % self.num_blocks])
for i in range(self.num_blocks * rounds)
]) / ((2**self.box_size)**(self.num_blocks * rounds)),
'linear': lambda rounds: 2**(self.num_blocks * rounds - 1) * Product([
self.sboxf(
self.inps[i // self.num_blocks][i % self.num_blocks],
self.oups[i // self.num_blocks][i % self.num_blocks])
for i in range(self.num_blocks * rounds)
]) / ((2**self.box_size)**(self.num_blocks * rounds))
}
def add_bias_constraints(self, prune_level):
"""Adds bias constraints to the solver based on the biases of the S-box.
This method adds constraints to the solver that are based on the biases of the S-box.
If the bias of a particular input-output pair is greater than or equal to 2**prune_level,
the method adds a constraint that the S-box function of the pair is equal to the bias.
Otherwise, it adds a constraint that the S-box function of the pair is 0. This helps in
pruning the search space of the solver.
Args:
prune_level (int): The level at which to prune the biases.
"""
for i in range(2**self.box_size):
for j in range(2**self.box_size):
# just some pruning of very small biases
if self.bias[(i, j)] >= 2**(prune_level):
self.solver.add(self.sboxf(i, j) == self.bias[(i, j)])
else:
self.solver.add(self.sboxf(i, j) == 0)
for r in range(self.num_rounds):
for i in range(self.num_blocks):
# skip taking input/outputs with no bias
self.solver.add(
Implies(
And(self.inps[r][i] != 0, self.oups[r][i] != 0),
self.sboxf(self.inps[r][i], self.oups[r][i]) != 0
)
)
def init_characteristic_solver(self, prune_level=-1):
"""Initializes the S-box structure, S-box function, objective functions, and pruning level.
This method initializes the structure of the S-box, the S-box function,
and the objective functions for the solver. It also sets the pruning level
for the solver. If no pruning level is provided, the method will search for
the best pruning level.
Args:
prune_level (int, optional): The level at which to prune the biases.
If not provided or less than 0, the method will search for the best pruning level.
"""
self.initialize_sbox_structure()
self.sboxf = Function(
'sbox', BitVecSort(
self.box_size), BitVecSort(
self.box_size), RealSort())
self.initialize_objectives()
assert self.solver.check()
if prune_level < 0:
print("searching best pruning level")
low, high = 0, len(self.sbox) // 4
while low <= high:
mid = (low + high) // 2
print("trying pruning", mid)
self.solver.push()
self.solver.set(timeout=10000)
self.add_bias_constraints(mid)
if self.solver.check() == sat:
print("success")
low = mid + 1
else:
print("failure")
high = mid - 1
self.solver.pop()
self.solver.set(timeout=0)
print("best pruning", high)
self.prune_level = high
self.add_bias_constraints(high)
else:
self.add_bias_constraints(prune_level)
if self.solver.check() == sat:
self.prune_level = prune_level
else:
print("Provided pruning level unsat, searching optimal pruning")
self.init_characteristic_solver(-1) # search best pruning
def solve_for_blocks(self, include_blocks=(), exclude_blocks=(),
num_rounds=0,
num_sols=1,
display_paths=True):
"""Solves the characteristic for the specified blocks and maximizes the objective function.
This method searches the characteristic for the specified blocks,
maximizes the objective function, and returns the solutions.
The blocks to include and exclude in the characteristic can be specified.
The number of rounds and the number of solutions can also be specified.
Args:
include_blocks (list, optional): The blocks to definitely include in the characteristic.
exclude_blocks (list, optional): The blocks to definitely exclude in the characteristic.
num_rounds (int, optional): The number of rounds for which to solve the characteristic.
If not provided or 0, the number of rounds will be set to the
number of rounds of the solver.
num_sols (int, optional): The number of solutions to return.
display_paths (bool, optional): Whether to display the paths of the solutions.
Returns:
list: A list of tuples. Each tuple contains the input masks, the output masks, and the
calculated bias for a solution.
"""
if num_rounds == 0:
num_rounds = self.num_rounds
else:
# cap to initialized struct
num_rounds = min(self.num_rounds, num_rounds)
while len(self.solver.objectives()):
self.solver.pop() # remove any previous include/exclude block constraints
self.solver.push() # set this as the checkpoint
# specify which blocks to definitely include in the characteristic
for i in include_blocks:
self.solver.add(self.inps[num_rounds - 1][i] != 0)
# specify which blocks to definitely exclude in the characteristic
for i in exclude_blocks:
self.solver.add(self.inps[num_rounds - 1][i] == 0)
# print(include_blocks, exclude_blocks)
# if a block is neither in include_blocks or exclude_blocks
# the solver finds the best path which may or may not set it to active
self.solver.maximize(self.objectives['reduced'](num_rounds))
solutions = self.get_masks(num_rounds, num_sols, display_paths)
self.solutions[(tuple(sorted(include_blocks)),
tuple(sorted(exclude_blocks)))].extend(solutions)
return [(inp_masks[0], inp_masks[-1], calc_bias)
for inp_masks, _, calc_bias, _ in solutions]
def search_best_masks(self, tolerance=1, choose_best=10, display_paths=True):
"""Searches for the best masks with the highest total bias and limited undiscovered active blocks.
This method searches for the best masks with the highest total bias and a limited number
of undiscovered active blocks.
Args:
tolerance (int, optional): The maximum number of undiscovered active blocks allowed.
choose_best (int, optional): The number of best masks to choose from.
display_paths (bool, optional): Whether to display the characteristic paths
(containing the bits involved) of the solutions.
Returns:
list: A list of tuples. Each tuple contains the input masks, the output masks, and the
total bias for a solution.
"""
self.init_characteristic_solver()
nr = self.num_rounds
discovered = [False for _ in range(self.num_blocks)]
def istolerable(x):
return sum((not i) and j
for i, j in zip(discovered, x[3])) in range(1, tolerance + 1)
masks = []
while self.solver.check() == sat:
curr_masks = self.get_masks(self.num_rounds, choose_best, display_paths=False)
for i in curr_masks:
self.solutions[i[2]].append(i)
curr_masks = list(filter(istolerable, curr_masks))
if len(curr_masks) > 0:
inp_masks, oup_masks, total_bias, active = max(
curr_masks, key=lambda x: (x[2], -sum(x[3])))
if display_paths:
self.print_bitrelations(inp_masks, oup_masks)
print("total bias:", total_bias)
print()
masks.append((inp_masks[0], inp_masks[nr - 1], total_bias))
for i, v in enumerate(discovered):
if (not v) and active[i]:
discovered[i] = True
print("discovered", "".join(map(lambda x: str(int(x)), discovered)))
# dont discover biases where all the active blocks come from discovered blocks
# i.e. if all the active blocks come from discovered blocks,
# it means, all the undiscovered blocks are inactive
# i.e it should not be the case where all the undiscovered blocks are
# inactive i.e 0
self.solver.add(Not(And(
[self.inps[nr - 1][i] == 0 for i, v in enumerate(discovered) if not v]
)))
return masks
def search_exclusive_masks(self, prune_level=-1, repeat=1):
"""Searches for the masks for each block by including only one block and excluding all the others.
This method searches for the masks for each block by including only one block and excluding
all the others.
Args:
prune_level (int, optional): The level at which to prune the biases.
repeat (int, optional): The number of times to repeat the search for each block.
Returns:
list: A list of tuples. Each tuple contains the input masks, the output masks, and the
total bias for a solution.
"""
self.init_characteristic_solver(prune_level)
masks = []
for i in range(self.num_blocks):
include_blocks = {i}
exclude_blocks = set(range(self.num_blocks)) - include_blocks
masks.extend(self.solve_for_blocks(include_blocks, exclude_blocks, num_sols=repeat))
return masks
def get_masks(self, num_rounds, n=1, display_paths=True):
"""Returns the input masks, output masks, total bias, and active blocks of the solutions.
This method returns the input masks, output masks, total bias, and active blocks of the solutions.
Args:
num_rounds (int): The number of rounds for which to get the masks.
n (int, optional): The number of masks to get.
display_paths (bool, optional): Whether to display the paths of the solutions.
Returns:
list: A list of tuples. Each tuple contains the input masks, the output masks, the total bias,
and the active blocks for a solution.
"""
masks = []
for m in islice(all_smt(self.solver, [self.bv_inp_masks[num_rounds - 1]]), n):
inp_masks = [m.eval(i).as_long()
for i in self.bv_inp_masks[:num_rounds]]
oup_masks = [m.eval(i).as_long()
for i in self.bv_oup_masks[:num_rounds]]
total_bias = m.eval(
self.objectives[self.mode](num_rounds)).as_fraction()
active = [m.eval(i).as_long() != 0 for i in self.inps[num_rounds - 1]]
if display_paths:
self.print_bitrelations(inp_masks, oup_masks)
print("total bias:", total_bias)
print()
masks.append((inp_masks, oup_masks, total_bias, active))
return masks
def print_bitrelations(self, inp_masks, out_masks):
"""
Print the input and output masks of a block cipher in a formatted manner.
:param inp_masks: List of integers, input masks for each round.
:param out_masks: List of integers, output masks for each round.
"""
s = self.box_size
n = self.num_blocks * s
def bin_sep(val):
v = bin(val)[2:].zfill(n)
return "|".join(v[i:i + s] for i in range(0, n, s))
rounds = len(out_masks)
for i in range(rounds):
imask, omask = inp_masks[i], out_masks[i]
print(bin_sep(imask))
print(' '.join(['-' * s] * (n // s)))
print(bin_sep(omask))
print()
print(bin_sep(inp_masks[-1]))
Classes
class CharacteristicSearcher (sbox, pbox, num_rounds, mode='linear')
-
A class for finding characteristics (linear or differential) of a substitution permutation network with provided S-box and P-box with a given number of rounds.
Attributes
sbox
- A list representing the substitution box.
pbox
- A list representing the permutation box.
num_rounds
- An integer representing the number of rounds.
block_size
- An integer representing the number of bits in the block.
box_size
- An integer representing the size of the S-box in bits.
num_blocks
- An integer representing the number of sboxes in a block
mode
- A string representing the mode, which can be 'linear' or 'differential'.
bias
- A Counter dictionary representing linear or differential bias of sbox input/output pairs
solutions
- A dictionary containing list of valid characteristic masks for a given set of included and excluded blocks
solver
- SMT solver (optimize) instance to search the characteristics
Initializes the CharacteristicSolver with the given sbox, pbox, num_rounds and mode.
Args
sbox
:list
- The substitution box.
pbox
:list
- The permutation box.
num_rounds
:int
- The number of rounds.
mode
:str
, optional- The mode of operation. Defaults to 'linear'.
Expand source code
class CharacteristicSearcher: """A class for finding characteristics (linear or differential) of a substitution permutation network with provided S-box and P-box with a given number of rounds. Attributes: sbox: A list representing the substitution box. pbox: A list representing the permutation box. num_rounds: An integer representing the number of rounds. block_size: An integer representing the number of bits in the block. box_size: An integer representing the size of the S-box in bits. num_blocks: An integer representing the number of sboxes in a block mode: A string representing the mode, which can be 'linear' or 'differential'. bias: A Counter dictionary representing linear or differential bias of sbox input/output pairs solutions: A dictionary containing list of valid characteristic masks for a given set of included and excluded blocks solver: SMT solver (optimize) instance to search the characteristics """ def __init__(self, sbox, pbox, num_rounds, mode='linear'): """Initializes the CharacteristicSolver with the given sbox, pbox, num_rounds and mode. Args: sbox (list): The substitution box. pbox (list): The permutation box. num_rounds (int): The number of rounds. mode (str, optional): The mode of operation. Defaults to 'linear'. """ self.sbox = sbox self.pbox = pbox self.num_rounds = num_rounds self.block_size = len(pbox) self.box_size = int(log2(len(sbox))) self.num_blocks = len(pbox) // self.box_size self.mode = mode if mode == 'linear': self.bias = calculate_linear_bias(sbox) elif mode == 'differential': self.bias = calculate_difference_table(sbox) self.solutions = defaultdict(list) self.solver = Optimize() self.prune_level = 0 self.sboxf = None self.inps = None self.oups = None self.bv_inp_masks = None self.bv_oup_masks = None self.objectives = None def initialize_sbox_structure(self): """Initializes the S-box structure for the cryptographic solver. This method sets up the structure of the S-box by creating an optimized solver, initializing input and output bit vectors for each round, and adding constraints for the solver. It also creates a concatenated view of the input and output layers for further processing. """ n = self.box_size self.inps = [[BitVec('r{}_i{}'.format(r, i), n) for i in range( self.num_blocks)] for r in range(self.num_rounds + 1)] self.oups = [[BitVec('r{}_o{}'.format(r, i), n) for i in range( self.num_blocks)] for r in range(self.num_rounds)] # permutation of output of sboxes are inputs of next round for i in range(self.num_rounds): if self.num_blocks == 1: self.solver.add(self.bitvec_permutation( self.oups[i][0], self.inps[i + 1][0])) else: self.solver.add(self.bitvec_permutation( Concat(self.oups[i]), Concat(self.inps[i + 1]))) # all first layer inputs should not be 0 self.solver.add( Not(And(*[self.inps[0][i] == 0 for i in range(self.num_blocks)]))) for r in range(self.num_rounds): for i in range(self.num_blocks): # if sbox has input, it should have output self.solver.add( Implies( self.inps[r][i] != 0, self.oups[r][i] != 0)) # if sbox has no input it should not have any output self.solver.add( Implies( self.inps[r][i] == 0, self.oups[r][i] == 0)) # just a concatanated view of the input and output layers if self.num_blocks == 1: self.bv_inp_masks = [self.inps[i][0] for i in range(self.num_rounds + 1)] self.bv_oup_masks = [self.oups[i][0] for i in range(self.num_rounds)] else: self.bv_inp_masks = [Concat(self.inps[i]) for i in range(self.num_rounds + 1)] self.bv_oup_masks = [Concat(self.oups[i]) for i in range(self.num_rounds)] def bitvec_permutation(self, inp, oup): """Performs bit vector permutation based on pbox. Args: inp (BitVec): The input bit vector. oup (BitVec): The output bit vector. Returns: list: A list of constraints for the permutation. """ pn = len(self.pbox) constraints = [] for i, v in enumerate(self.pbox): constraints.append( Extract(pn - 1 - i, pn - 1 - i, inp) == Extract(pn - 1 - v, pn - 1 - v, oup) ) return constraints def initialize_objectives(self): """Initializes the objective functions for the cryptographic solver. The method sets up four types of objective functions: 'original_linear', 'reduced', 'differential', and 'linear'. These objective functions are used to guide the solver in finding the optimal solution. Each objective function is associated with a lambda function that calculates the objective value for a given number of rounds. 'reduced' objective is called for both linear and differential search Other objective functions are just there for reference and easy evaluation of bias directly from the model """ self.objectives = { # the actual objective, which is just product of bias [0,1/2] 'original_linear': lambda rounds: 2**(self.num_blocks * rounds - 1) * Product([self.sboxf( self.inps[i // self.num_blocks][i % self.num_blocks], self.oups[i // self.num_blocks][i % self.num_blocks]) for i in range(self.num_blocks * rounds) ]), # reducing optimization problem of product to sums 'reduced': lambda rounds: sum([ self.sboxf( self.inps[i // self.num_blocks][i % self.num_blocks], self.oups[i // self.num_blocks][i % self.num_blocks]) for i in range(self.num_blocks * rounds) ]), # objective when the input biases are [0,2**n] just the final # division 'differential': lambda rounds: Product([ self.sboxf( self.inps[i // self.num_blocks][i % self.num_blocks], self.oups[i // self.num_blocks][i % self.num_blocks]) for i in range(self.num_blocks * rounds) ]) / ((2**self.box_size)**(self.num_blocks * rounds)), 'linear': lambda rounds: 2**(self.num_blocks * rounds - 1) * Product([ self.sboxf( self.inps[i // self.num_blocks][i % self.num_blocks], self.oups[i // self.num_blocks][i % self.num_blocks]) for i in range(self.num_blocks * rounds) ]) / ((2**self.box_size)**(self.num_blocks * rounds)) } def add_bias_constraints(self, prune_level): """Adds bias constraints to the solver based on the biases of the S-box. This method adds constraints to the solver that are based on the biases of the S-box. If the bias of a particular input-output pair is greater than or equal to 2**prune_level, the method adds a constraint that the S-box function of the pair is equal to the bias. Otherwise, it adds a constraint that the S-box function of the pair is 0. This helps in pruning the search space of the solver. Args: prune_level (int): The level at which to prune the biases. """ for i in range(2**self.box_size): for j in range(2**self.box_size): # just some pruning of very small biases if self.bias[(i, j)] >= 2**(prune_level): self.solver.add(self.sboxf(i, j) == self.bias[(i, j)]) else: self.solver.add(self.sboxf(i, j) == 0) for r in range(self.num_rounds): for i in range(self.num_blocks): # skip taking input/outputs with no bias self.solver.add( Implies( And(self.inps[r][i] != 0, self.oups[r][i] != 0), self.sboxf(self.inps[r][i], self.oups[r][i]) != 0 ) ) def init_characteristic_solver(self, prune_level=-1): """Initializes the S-box structure, S-box function, objective functions, and pruning level. This method initializes the structure of the S-box, the S-box function, and the objective functions for the solver. It also sets the pruning level for the solver. If no pruning level is provided, the method will search for the best pruning level. Args: prune_level (int, optional): The level at which to prune the biases. If not provided or less than 0, the method will search for the best pruning level. """ self.initialize_sbox_structure() self.sboxf = Function( 'sbox', BitVecSort( self.box_size), BitVecSort( self.box_size), RealSort()) self.initialize_objectives() assert self.solver.check() if prune_level < 0: print("searching best pruning level") low, high = 0, len(self.sbox) // 4 while low <= high: mid = (low + high) // 2 print("trying pruning", mid) self.solver.push() self.solver.set(timeout=10000) self.add_bias_constraints(mid) if self.solver.check() == sat: print("success") low = mid + 1 else: print("failure") high = mid - 1 self.solver.pop() self.solver.set(timeout=0) print("best pruning", high) self.prune_level = high self.add_bias_constraints(high) else: self.add_bias_constraints(prune_level) if self.solver.check() == sat: self.prune_level = prune_level else: print("Provided pruning level unsat, searching optimal pruning") self.init_characteristic_solver(-1) # search best pruning def solve_for_blocks(self, include_blocks=(), exclude_blocks=(), num_rounds=0, num_sols=1, display_paths=True): """Solves the characteristic for the specified blocks and maximizes the objective function. This method searches the characteristic for the specified blocks, maximizes the objective function, and returns the solutions. The blocks to include and exclude in the characteristic can be specified. The number of rounds and the number of solutions can also be specified. Args: include_blocks (list, optional): The blocks to definitely include in the characteristic. exclude_blocks (list, optional): The blocks to definitely exclude in the characteristic. num_rounds (int, optional): The number of rounds for which to solve the characteristic. If not provided or 0, the number of rounds will be set to the number of rounds of the solver. num_sols (int, optional): The number of solutions to return. display_paths (bool, optional): Whether to display the paths of the solutions. Returns: list: A list of tuples. Each tuple contains the input masks, the output masks, and the calculated bias for a solution. """ if num_rounds == 0: num_rounds = self.num_rounds else: # cap to initialized struct num_rounds = min(self.num_rounds, num_rounds) while len(self.solver.objectives()): self.solver.pop() # remove any previous include/exclude block constraints self.solver.push() # set this as the checkpoint # specify which blocks to definitely include in the characteristic for i in include_blocks: self.solver.add(self.inps[num_rounds - 1][i] != 0) # specify which blocks to definitely exclude in the characteristic for i in exclude_blocks: self.solver.add(self.inps[num_rounds - 1][i] == 0) # print(include_blocks, exclude_blocks) # if a block is neither in include_blocks or exclude_blocks # the solver finds the best path which may or may not set it to active self.solver.maximize(self.objectives['reduced'](num_rounds)) solutions = self.get_masks(num_rounds, num_sols, display_paths) self.solutions[(tuple(sorted(include_blocks)), tuple(sorted(exclude_blocks)))].extend(solutions) return [(inp_masks[0], inp_masks[-1], calc_bias) for inp_masks, _, calc_bias, _ in solutions] def search_best_masks(self, tolerance=1, choose_best=10, display_paths=True): """Searches for the best masks with the highest total bias and limited undiscovered active blocks. This method searches for the best masks with the highest total bias and a limited number of undiscovered active blocks. Args: tolerance (int, optional): The maximum number of undiscovered active blocks allowed. choose_best (int, optional): The number of best masks to choose from. display_paths (bool, optional): Whether to display the characteristic paths (containing the bits involved) of the solutions. Returns: list: A list of tuples. Each tuple contains the input masks, the output masks, and the total bias for a solution. """ self.init_characteristic_solver() nr = self.num_rounds discovered = [False for _ in range(self.num_blocks)] def istolerable(x): return sum((not i) and j for i, j in zip(discovered, x[3])) in range(1, tolerance + 1) masks = [] while self.solver.check() == sat: curr_masks = self.get_masks(self.num_rounds, choose_best, display_paths=False) for i in curr_masks: self.solutions[i[2]].append(i) curr_masks = list(filter(istolerable, curr_masks)) if len(curr_masks) > 0: inp_masks, oup_masks, total_bias, active = max( curr_masks, key=lambda x: (x[2], -sum(x[3]))) if display_paths: self.print_bitrelations(inp_masks, oup_masks) print("total bias:", total_bias) print() masks.append((inp_masks[0], inp_masks[nr - 1], total_bias)) for i, v in enumerate(discovered): if (not v) and active[i]: discovered[i] = True print("discovered", "".join(map(lambda x: str(int(x)), discovered))) # dont discover biases where all the active blocks come from discovered blocks # i.e. if all the active blocks come from discovered blocks, # it means, all the undiscovered blocks are inactive # i.e it should not be the case where all the undiscovered blocks are # inactive i.e 0 self.solver.add(Not(And( [self.inps[nr - 1][i] == 0 for i, v in enumerate(discovered) if not v] ))) return masks def search_exclusive_masks(self, prune_level=-1, repeat=1): """Searches for the masks for each block by including only one block and excluding all the others. This method searches for the masks for each block by including only one block and excluding all the others. Args: prune_level (int, optional): The level at which to prune the biases. repeat (int, optional): The number of times to repeat the search for each block. Returns: list: A list of tuples. Each tuple contains the input masks, the output masks, and the total bias for a solution. """ self.init_characteristic_solver(prune_level) masks = [] for i in range(self.num_blocks): include_blocks = {i} exclude_blocks = set(range(self.num_blocks)) - include_blocks masks.extend(self.solve_for_blocks(include_blocks, exclude_blocks, num_sols=repeat)) return masks def get_masks(self, num_rounds, n=1, display_paths=True): """Returns the input masks, output masks, total bias, and active blocks of the solutions. This method returns the input masks, output masks, total bias, and active blocks of the solutions. Args: num_rounds (int): The number of rounds for which to get the masks. n (int, optional): The number of masks to get. display_paths (bool, optional): Whether to display the paths of the solutions. Returns: list: A list of tuples. Each tuple contains the input masks, the output masks, the total bias, and the active blocks for a solution. """ masks = [] for m in islice(all_smt(self.solver, [self.bv_inp_masks[num_rounds - 1]]), n): inp_masks = [m.eval(i).as_long() for i in self.bv_inp_masks[:num_rounds]] oup_masks = [m.eval(i).as_long() for i in self.bv_oup_masks[:num_rounds]] total_bias = m.eval( self.objectives[self.mode](num_rounds)).as_fraction() active = [m.eval(i).as_long() != 0 for i in self.inps[num_rounds - 1]] if display_paths: self.print_bitrelations(inp_masks, oup_masks) print("total bias:", total_bias) print() masks.append((inp_masks, oup_masks, total_bias, active)) return masks def print_bitrelations(self, inp_masks, out_masks): """ Print the input and output masks of a block cipher in a formatted manner. :param inp_masks: List of integers, input masks for each round. :param out_masks: List of integers, output masks for each round. """ s = self.box_size n = self.num_blocks * s def bin_sep(val): v = bin(val)[2:].zfill(n) return "|".join(v[i:i + s] for i in range(0, n, s)) rounds = len(out_masks) for i in range(rounds): imask, omask = inp_masks[i], out_masks[i] print(bin_sep(imask)) print(' '.join(['-' * s] * (n // s))) print(bin_sep(omask)) print() print(bin_sep(inp_masks[-1]))
Methods
def add_bias_constraints(self, prune_level)
-
Adds bias constraints to the solver based on the biases of the S-box.
This method adds constraints to the solver that are based on the biases of the S-box. If the bias of a particular input-output pair is greater than or equal to 2**prune_level, the method adds a constraint that the S-box function of the pair is equal to the bias. Otherwise, it adds a constraint that the S-box function of the pair is 0. This helps in pruning the search space of the solver.
Args
prune_level
:int
- The level at which to prune the biases.
Expand source code
def add_bias_constraints(self, prune_level): """Adds bias constraints to the solver based on the biases of the S-box. This method adds constraints to the solver that are based on the biases of the S-box. If the bias of a particular input-output pair is greater than or equal to 2**prune_level, the method adds a constraint that the S-box function of the pair is equal to the bias. Otherwise, it adds a constraint that the S-box function of the pair is 0. This helps in pruning the search space of the solver. Args: prune_level (int): The level at which to prune the biases. """ for i in range(2**self.box_size): for j in range(2**self.box_size): # just some pruning of very small biases if self.bias[(i, j)] >= 2**(prune_level): self.solver.add(self.sboxf(i, j) == self.bias[(i, j)]) else: self.solver.add(self.sboxf(i, j) == 0) for r in range(self.num_rounds): for i in range(self.num_blocks): # skip taking input/outputs with no bias self.solver.add( Implies( And(self.inps[r][i] != 0, self.oups[r][i] != 0), self.sboxf(self.inps[r][i], self.oups[r][i]) != 0 ) )
def bitvec_permutation(self, inp, oup)
-
Performs bit vector permutation based on pbox.
Args
inp
:BitVec
- The input bit vector.
oup
:BitVec
- The output bit vector.
Returns
list
- A list of constraints for the permutation.
Expand source code
def bitvec_permutation(self, inp, oup): """Performs bit vector permutation based on pbox. Args: inp (BitVec): The input bit vector. oup (BitVec): The output bit vector. Returns: list: A list of constraints for the permutation. """ pn = len(self.pbox) constraints = [] for i, v in enumerate(self.pbox): constraints.append( Extract(pn - 1 - i, pn - 1 - i, inp) == Extract(pn - 1 - v, pn - 1 - v, oup) ) return constraints
def get_masks(self, num_rounds, n=1, display_paths=True)
-
Returns the input masks, output masks, total bias, and active blocks of the solutions.
This method returns the input masks, output masks, total bias, and active blocks of the solutions.
Args
num_rounds
:int
- The number of rounds for which to get the masks.
n
:int
, optional- The number of masks to get.
display_paths
:bool
, optional- Whether to display the paths of the solutions.
Returns
list
- A list of tuples. Each tuple contains the input masks, the output masks, the total bias, and the active blocks for a solution.
Expand source code
def get_masks(self, num_rounds, n=1, display_paths=True): """Returns the input masks, output masks, total bias, and active blocks of the solutions. This method returns the input masks, output masks, total bias, and active blocks of the solutions. Args: num_rounds (int): The number of rounds for which to get the masks. n (int, optional): The number of masks to get. display_paths (bool, optional): Whether to display the paths of the solutions. Returns: list: A list of tuples. Each tuple contains the input masks, the output masks, the total bias, and the active blocks for a solution. """ masks = [] for m in islice(all_smt(self.solver, [self.bv_inp_masks[num_rounds - 1]]), n): inp_masks = [m.eval(i).as_long() for i in self.bv_inp_masks[:num_rounds]] oup_masks = [m.eval(i).as_long() for i in self.bv_oup_masks[:num_rounds]] total_bias = m.eval( self.objectives[self.mode](num_rounds)).as_fraction() active = [m.eval(i).as_long() != 0 for i in self.inps[num_rounds - 1]] if display_paths: self.print_bitrelations(inp_masks, oup_masks) print("total bias:", total_bias) print() masks.append((inp_masks, oup_masks, total_bias, active)) return masks
def init_characteristic_solver(self, prune_level=-1)
-
Initializes the S-box structure, S-box function, objective functions, and pruning level.
This method initializes the structure of the S-box, the S-box function, and the objective functions for the solver. It also sets the pruning level for the solver. If no pruning level is provided, the method will search for the best pruning level.
Args
prune_level
:int
, optional- The level at which to prune the biases.
If not provided or less than 0, the method will search for the best pruning level.
Expand source code
def init_characteristic_solver(self, prune_level=-1): """Initializes the S-box structure, S-box function, objective functions, and pruning level. This method initializes the structure of the S-box, the S-box function, and the objective functions for the solver. It also sets the pruning level for the solver. If no pruning level is provided, the method will search for the best pruning level. Args: prune_level (int, optional): The level at which to prune the biases. If not provided or less than 0, the method will search for the best pruning level. """ self.initialize_sbox_structure() self.sboxf = Function( 'sbox', BitVecSort( self.box_size), BitVecSort( self.box_size), RealSort()) self.initialize_objectives() assert self.solver.check() if prune_level < 0: print("searching best pruning level") low, high = 0, len(self.sbox) // 4 while low <= high: mid = (low + high) // 2 print("trying pruning", mid) self.solver.push() self.solver.set(timeout=10000) self.add_bias_constraints(mid) if self.solver.check() == sat: print("success") low = mid + 1 else: print("failure") high = mid - 1 self.solver.pop() self.solver.set(timeout=0) print("best pruning", high) self.prune_level = high self.add_bias_constraints(high) else: self.add_bias_constraints(prune_level) if self.solver.check() == sat: self.prune_level = prune_level else: print("Provided pruning level unsat, searching optimal pruning") self.init_characteristic_solver(-1) # search best pruning
def initialize_objectives(self)
-
Initializes the objective functions for the cryptographic solver.
The method sets up four types of objective functions: 'original_linear', 'reduced', 'differential', and 'linear'. These objective functions are used to guide the solver in finding the optimal solution. Each objective function is associated with a lambda function that calculates the objective value for a given number of rounds. 'reduced' objective is called for both linear and differential search Other objective functions are just there for reference and easy evaluation of bias directly from the model
Expand source code
def initialize_objectives(self): """Initializes the objective functions for the cryptographic solver. The method sets up four types of objective functions: 'original_linear', 'reduced', 'differential', and 'linear'. These objective functions are used to guide the solver in finding the optimal solution. Each objective function is associated with a lambda function that calculates the objective value for a given number of rounds. 'reduced' objective is called for both linear and differential search Other objective functions are just there for reference and easy evaluation of bias directly from the model """ self.objectives = { # the actual objective, which is just product of bias [0,1/2] 'original_linear': lambda rounds: 2**(self.num_blocks * rounds - 1) * Product([self.sboxf( self.inps[i // self.num_blocks][i % self.num_blocks], self.oups[i // self.num_blocks][i % self.num_blocks]) for i in range(self.num_blocks * rounds) ]), # reducing optimization problem of product to sums 'reduced': lambda rounds: sum([ self.sboxf( self.inps[i // self.num_blocks][i % self.num_blocks], self.oups[i // self.num_blocks][i % self.num_blocks]) for i in range(self.num_blocks * rounds) ]), # objective when the input biases are [0,2**n] just the final # division 'differential': lambda rounds: Product([ self.sboxf( self.inps[i // self.num_blocks][i % self.num_blocks], self.oups[i // self.num_blocks][i % self.num_blocks]) for i in range(self.num_blocks * rounds) ]) / ((2**self.box_size)**(self.num_blocks * rounds)), 'linear': lambda rounds: 2**(self.num_blocks * rounds - 1) * Product([ self.sboxf( self.inps[i // self.num_blocks][i % self.num_blocks], self.oups[i // self.num_blocks][i % self.num_blocks]) for i in range(self.num_blocks * rounds) ]) / ((2**self.box_size)**(self.num_blocks * rounds)) }
def initialize_sbox_structure(self)
-
Initializes the S-box structure for the cryptographic solver.
This method sets up the structure of the S-box by creating an optimized solver, initializing input and output bit vectors for each round, and adding constraints for the solver. It also creates a concatenated view of the input and output layers for further processing.
Expand source code
def initialize_sbox_structure(self): """Initializes the S-box structure for the cryptographic solver. This method sets up the structure of the S-box by creating an optimized solver, initializing input and output bit vectors for each round, and adding constraints for the solver. It also creates a concatenated view of the input and output layers for further processing. """ n = self.box_size self.inps = [[BitVec('r{}_i{}'.format(r, i), n) for i in range( self.num_blocks)] for r in range(self.num_rounds + 1)] self.oups = [[BitVec('r{}_o{}'.format(r, i), n) for i in range( self.num_blocks)] for r in range(self.num_rounds)] # permutation of output of sboxes are inputs of next round for i in range(self.num_rounds): if self.num_blocks == 1: self.solver.add(self.bitvec_permutation( self.oups[i][0], self.inps[i + 1][0])) else: self.solver.add(self.bitvec_permutation( Concat(self.oups[i]), Concat(self.inps[i + 1]))) # all first layer inputs should not be 0 self.solver.add( Not(And(*[self.inps[0][i] == 0 for i in range(self.num_blocks)]))) for r in range(self.num_rounds): for i in range(self.num_blocks): # if sbox has input, it should have output self.solver.add( Implies( self.inps[r][i] != 0, self.oups[r][i] != 0)) # if sbox has no input it should not have any output self.solver.add( Implies( self.inps[r][i] == 0, self.oups[r][i] == 0)) # just a concatanated view of the input and output layers if self.num_blocks == 1: self.bv_inp_masks = [self.inps[i][0] for i in range(self.num_rounds + 1)] self.bv_oup_masks = [self.oups[i][0] for i in range(self.num_rounds)] else: self.bv_inp_masks = [Concat(self.inps[i]) for i in range(self.num_rounds + 1)] self.bv_oup_masks = [Concat(self.oups[i]) for i in range(self.num_rounds)]
def print_bitrelations(self, inp_masks, out_masks)
-
Print the input and output masks of a block cipher in a formatted manner.
:param inp_masks: List of integers, input masks for each round. :param out_masks: List of integers, output masks for each round.
Expand source code
def print_bitrelations(self, inp_masks, out_masks): """ Print the input and output masks of a block cipher in a formatted manner. :param inp_masks: List of integers, input masks for each round. :param out_masks: List of integers, output masks for each round. """ s = self.box_size n = self.num_blocks * s def bin_sep(val): v = bin(val)[2:].zfill(n) return "|".join(v[i:i + s] for i in range(0, n, s)) rounds = len(out_masks) for i in range(rounds): imask, omask = inp_masks[i], out_masks[i] print(bin_sep(imask)) print(' '.join(['-' * s] * (n // s))) print(bin_sep(omask)) print() print(bin_sep(inp_masks[-1]))
def search_best_masks(self, tolerance=1, choose_best=10, display_paths=True)
-
Searches for the best masks with the highest total bias and limited undiscovered active blocks.
This method searches for the best masks with the highest total bias and a limited number of undiscovered active blocks.
Args
tolerance
:int
, optional- The maximum number of undiscovered active blocks allowed.
choose_best
:int
, optional- The number of best masks to choose from.
display_paths
:bool
, optional- Whether to display the characteristic paths (containing the bits involved) of the solutions.
Returns
list
- A list of tuples. Each tuple contains the input masks, the output masks, and the total bias for a solution.
Expand source code
def search_best_masks(self, tolerance=1, choose_best=10, display_paths=True): """Searches for the best masks with the highest total bias and limited undiscovered active blocks. This method searches for the best masks with the highest total bias and a limited number of undiscovered active blocks. Args: tolerance (int, optional): The maximum number of undiscovered active blocks allowed. choose_best (int, optional): The number of best masks to choose from. display_paths (bool, optional): Whether to display the characteristic paths (containing the bits involved) of the solutions. Returns: list: A list of tuples. Each tuple contains the input masks, the output masks, and the total bias for a solution. """ self.init_characteristic_solver() nr = self.num_rounds discovered = [False for _ in range(self.num_blocks)] def istolerable(x): return sum((not i) and j for i, j in zip(discovered, x[3])) in range(1, tolerance + 1) masks = [] while self.solver.check() == sat: curr_masks = self.get_masks(self.num_rounds, choose_best, display_paths=False) for i in curr_masks: self.solutions[i[2]].append(i) curr_masks = list(filter(istolerable, curr_masks)) if len(curr_masks) > 0: inp_masks, oup_masks, total_bias, active = max( curr_masks, key=lambda x: (x[2], -sum(x[3]))) if display_paths: self.print_bitrelations(inp_masks, oup_masks) print("total bias:", total_bias) print() masks.append((inp_masks[0], inp_masks[nr - 1], total_bias)) for i, v in enumerate(discovered): if (not v) and active[i]: discovered[i] = True print("discovered", "".join(map(lambda x: str(int(x)), discovered))) # dont discover biases where all the active blocks come from discovered blocks # i.e. if all the active blocks come from discovered blocks, # it means, all the undiscovered blocks are inactive # i.e it should not be the case where all the undiscovered blocks are # inactive i.e 0 self.solver.add(Not(And( [self.inps[nr - 1][i] == 0 for i, v in enumerate(discovered) if not v] ))) return masks
def search_exclusive_masks(self, prune_level=-1, repeat=1)
-
Searches for the masks for each block by including only one block and excluding all the others.
This method searches for the masks for each block by including only one block and excluding all the others.
Args
prune_level
:int
, optional- The level at which to prune the biases.
repeat
:int
, optional- The number of times to repeat the search for each block.
Returns
list
- A list of tuples. Each tuple contains the input masks, the output masks, and the total bias for a solution.
Expand source code
def search_exclusive_masks(self, prune_level=-1, repeat=1): """Searches for the masks for each block by including only one block and excluding all the others. This method searches for the masks for each block by including only one block and excluding all the others. Args: prune_level (int, optional): The level at which to prune the biases. repeat (int, optional): The number of times to repeat the search for each block. Returns: list: A list of tuples. Each tuple contains the input masks, the output masks, and the total bias for a solution. """ self.init_characteristic_solver(prune_level) masks = [] for i in range(self.num_blocks): include_blocks = {i} exclude_blocks = set(range(self.num_blocks)) - include_blocks masks.extend(self.solve_for_blocks(include_blocks, exclude_blocks, num_sols=repeat)) return masks
def solve_for_blocks(self, include_blocks=(), exclude_blocks=(), num_rounds=0, num_sols=1, display_paths=True)
-
Solves the characteristic for the specified blocks and maximizes the objective function.
This method searches the characteristic for the specified blocks, maximizes the objective function, and returns the solutions. The blocks to include and exclude in the characteristic can be specified. The number of rounds and the number of solutions can also be specified.
Args
include_blocks
:list
, optional- The blocks to definitely include in the characteristic.
exclude_blocks
:list
, optional- The blocks to definitely exclude in the characteristic.
num_rounds
:int
, optional- The number of rounds for which to solve the characteristic. If not provided or 0, the number of rounds will be set to the number of rounds of the solver.
num_sols
:int
, optional- The number of solutions to return.
display_paths
:bool
, optional- Whether to display the paths of the solutions.
Returns
list
- A list of tuples. Each tuple contains the input masks, the output masks, and the calculated bias for a solution.
Expand source code
def solve_for_blocks(self, include_blocks=(), exclude_blocks=(), num_rounds=0, num_sols=1, display_paths=True): """Solves the characteristic for the specified blocks and maximizes the objective function. This method searches the characteristic for the specified blocks, maximizes the objective function, and returns the solutions. The blocks to include and exclude in the characteristic can be specified. The number of rounds and the number of solutions can also be specified. Args: include_blocks (list, optional): The blocks to definitely include in the characteristic. exclude_blocks (list, optional): The blocks to definitely exclude in the characteristic. num_rounds (int, optional): The number of rounds for which to solve the characteristic. If not provided or 0, the number of rounds will be set to the number of rounds of the solver. num_sols (int, optional): The number of solutions to return. display_paths (bool, optional): Whether to display the paths of the solutions. Returns: list: A list of tuples. Each tuple contains the input masks, the output masks, and the calculated bias for a solution. """ if num_rounds == 0: num_rounds = self.num_rounds else: # cap to initialized struct num_rounds = min(self.num_rounds, num_rounds) while len(self.solver.objectives()): self.solver.pop() # remove any previous include/exclude block constraints self.solver.push() # set this as the checkpoint # specify which blocks to definitely include in the characteristic for i in include_blocks: self.solver.add(self.inps[num_rounds - 1][i] != 0) # specify which blocks to definitely exclude in the characteristic for i in exclude_blocks: self.solver.add(self.inps[num_rounds - 1][i] == 0) # print(include_blocks, exclude_blocks) # if a block is neither in include_blocks or exclude_blocks # the solver finds the best path which may or may not set it to active self.solver.maximize(self.objectives['reduced'](num_rounds)) solutions = self.get_masks(num_rounds, num_sols, display_paths) self.solutions[(tuple(sorted(include_blocks)), tuple(sorted(exclude_blocks)))].extend(solutions) return [(inp_masks[0], inp_masks[-1], calc_bias) for inp_masks, _, calc_bias, _ in solutions]