arxpy/differential/characteristic.py
class RelatedKeyCh(object):
    """Represent related-key characteristics of block ciphers.

    A related-key characteristic of a `Cipher` is a pair `BvCharacteristic`,
    one over the `KeySchedule` of the cipher, and another one over the
    the `Encryption` function of the cipher, where the output differences
    of the key schedule characteristic are used as round key differences
    in the encryption characteristic.

    The master key differences start with the prefix ``"dmk"``,
    the round key differences start with the prefix ``"dk"``,
    the plaintext differences start with the prefix ``"dp"``
    and the non-linear differences start with the prefix ``"dx"``.

        >>> from arxpy.bitvector.core import Variable
        >>> from arxpy.differential.difference import XorDiff
        >>> from arxpy.differential.characteristic import RelatedKeyCh
        >>> from arxpy.primitives.primitives import Cipher
        >>> from arxpy.primitives.lea import LeaCipher
        >>> issubclass(LeaCipher, Cipher)
        True
        >>> LeaCipher.set_rounds(1)
        >>> rkch = RelatedKeyCh(LeaCipher, XorDiff)
        >>> rkch .key_schedule_ch.ssa  # doctest: +NORMALIZE_WHITESPACE
        {'input_vars': (dmk0, dmk1, dmk2, dmk3),
        'output_vars': (dk1, dk3, dk5, dk3, dk7, dk3),
        'assignments': ((dk0, 0xc3efe9db + dmk0), (dk1, dk0 <<< 1), (dk2, 0x87dfd3b7 + dmk1), (dk3, dk2 <<< 3),
        (dk4, 0x0fbfa76f + dmk2), (dk5, dk4 <<< 6), (dk6, 0x1f7f4ede + dmk3), (dk7, dk6 <<< 11))}
        >>> rkch.key_schedule_ch.input_diff
        (XorDiff(dmk0), XorDiff(dmk1), XorDiff(dmk2), XorDiff(dmk3))
        >>> rkch.key_schedule_ch.output_diff  # doctest: +NORMALIZE_WHITESPACE
        [[XorDiff(dk1), XorDiff(dk0 <<< 1)], [XorDiff(dk3), XorDiff(dk2 <<< 3)],
        [XorDiff(dk5), XorDiff(dk4 <<< 6)], [XorDiff(dk3), XorDiff(dk2 <<< 3)],
        [XorDiff(dk7), XorDiff(dk6 <<< 11)], [XorDiff(dk3), XorDiff(dk2 <<< 3)]]
        >>> rkch.key_schedule_ch.nonlinear_diffs  # doctest: +NORMALIZE_WHITESPACE
        OrderedDict([(XorDiff(dk0), XDCA_0xc3efe9db(XorDiff(dmk0))),
        (XorDiff(dk2), XDCA_0x87dfd3b7(XorDiff(dmk1))),
        (XorDiff(dk4), XDCA_0x0fbfa76f(XorDiff(dmk2))),
        (XorDiff(dk6), XDCA_0x1f7f4ede(XorDiff(dmk3)))])
        >>> rkch.encryption_ch.ssa  # doctest: +NORMALIZE_WHITESPACE
        {'input_vars': (dp0, dp1, dp2, dp3),
        'output_vars': (dx3, dx7, dx11, dp0),
        'assignments': ((dx0, dk1 ^ dp0), (dx1, dk3 ^ dp1), (dx2, dx0 + dx1), (dx3, dx2 <<< 9),
        (dx4, dk5 ^ dp1), (dx5, dk3 ^ dp2), (dx6, dx4 + dx5), (dx7, dx6 >>> 5), (dx8, dk7 ^ dp2),
        (dx9, dk3 ^ dp3), (dx10, dx8 + dx9), (dx11, dx10 >>> 3))}
        >>> rkch.encryption_ch.input_diff
        (XorDiff(dp0), XorDiff(dp1), XorDiff(dp2), XorDiff(dp3))
        >>> rkch.encryption_ch.output_diff # doctest: +NORMALIZE_WHITESPACE
        [[XorDiff(dx3), XorDiff(dx2 <<< 9)], [XorDiff(dx7), XorDiff(dx6 >>> 5)],
        [XorDiff(dx11), XorDiff(dx10 >>> 3)], [XorDiff(dp0), XorDiff(dp0)]]
        >>> rkch.encryption_ch.nonlinear_diffs  # doctest: +NORMALIZE_WHITESPACE
        OrderedDict([(XorDiff(dx2), XDA(XorDiff(dp0 ^ (dk0 <<< 1)), XorDiff(dp1 ^ (dk2 <<< 3)))),
        (XorDiff(dx6), XDA(XorDiff(dp1 ^ (dk4 <<< 6)), XorDiff(dp2 ^ (dk2 <<< 3)))),
        (XorDiff(dx10), XDA(XorDiff(dp2 ^ (dk6 <<< 11)), XorDiff(dp3 ^ (dk2 <<< 3))))])

    Attributes:
        key_schedule_ch: the `BvCharacteristic` over the key schedule
        encryption_ch: the `BvCharacteristic` over the encryption function
    """

    def __init__(self, bv_cipher, diff_type):
        assert issubclass(bv_cipher, primitives.Cipher)
        assert issubclass(diff_type, difference.Difference)

        func = bv_cipher.key_schedule
        prefix = "dk"
        input_diff_names = tuple(["dmk" + str(i) for i in range(len(func.input_widths))])
        ks_ch = BvCharacteristic(func, diff_type, input_diff_names, prefix)

        class Encryption(bv_cipher.encryption):
            round_keys = ks_ch.ssa["output_vars"]

        func = Encryption
        prefix = "dx"
        input_diff_names = ["dp" + str(i) for i in range(len(func.input_widths))]
        round_key_diff = {}
        for var, diff in ks_ch.output_diff:
            round_key_diff[var.val] = diff
        encryption_ch = BvCharacteristic(func, diff_type, input_diff_names,
                                         prefix, round_key_diff)

        self.diff_type = diff_type
        self.key_schedule_ch = ks_ch
        self.encryption_ch = encryption_ch
        self._cipher = bv_cipher

    def empirical_weight(self, key_input_diff, key_output_diff, key_samples,
                         enc_input_diff, enc_output_diff, enc_samples, precision=1):
        """Return the empirical weight of a given differential for multiple keys.

        This method returns the differential probability weight for the
        key schedule characteristic (see `BvCharacteristic.empirical_weight`)
        and the `collections.Counter` storing the distribution of weights for the encryption
        characteristic (see `SingleKeyCh.empirical_weight`).

            >>> from arxpy.bitvector.core import Variable, Constant
            >>> from arxpy.differential.difference import XorDiff
            >>> from arxpy.differential.characteristic import RelatedKeyCh
            >>> from arxpy.primitives.lea import LeaCipher
            >>> LeaCipher.set_rounds(1)
            >>> rkch = RelatedKeyCh(LeaCipher, XorDiff)
            >>> zero, one = XorDiff(Constant(0, 32)), XorDiff(Constant(1, 32))
            >>> kid, kod = [zero]*4, [zero]*6
            >>> eid, eod = [zero]*4, [zero]*4
            >>> rkch.empirical_weight(kid, kod, 10, eid, eod, 100)
            (0.0, Counter({0.0: 10}))
            >>> kid, kod = [zero]*4, [one]*6
            >>> eid, eod = [zero]*4, [one]*4
            >>> rkch.empirical_weight(kid, kod, 10, eid, eod, 100)
            (inf, Counter({inf: 10}))

        """
        key_weight = self.key_schedule_ch.empirical_weight(key_input_diff, key_output_diff, key_samples)
        # noinspection PyProtectedMember
        enc_counter = self.encryption_ch._empirical_weight_distribution(self._cipher,
                                                                        enc_input_diff, enc_output_diff, enc_samples,
                                                                        key_samples, precision, key_output_diff)
        return key_weight, enc_counter

    def signature(self, ch_signature_type):
        """Return the signature of the related-key characteristic.

        The signature of a related-key characteristic is the
        concatenation of the key schedule and encryption signatures.

        See also `BvCharacteristic.signature`.

            >>> from arxpy.bitvector.core import Variable
            >>> from arxpy.differential.difference import XorDiff
            >>> from arxpy.differential.characteristic import RelatedKeyCh, ChSignatureType
            >>> from arxpy.primitives.primitives import Cipher
            >>> from arxpy.primitives.lea import LeaCipher
            >>> LeaCipher.set_rounds(1)
            >>> rkch = RelatedKeyCh(LeaCipher, XorDiff)
            >>> rkch.signature(ChSignatureType.Full)  # doctest:+NORMALIZE_WHITESPACE
            [dmk0, dmk1, dmk2, dmk3, dk0, dk2, dk4, dk6, dp0, dp1, dp2, dp3, dx2, dx6, dx10]
            >>> rkch.signature(ChSignatureType.InputOutput)  # doctest:+NORMALIZE_WHITESPACE
            [dmk0, dmk1, dmk2, dmk3, dk1, dk3, dk5, dk3, dk7, dk3, dp0, dp1, dp2, dp3, dx3, dx7, dx11]

        """
        return self.key_schedule_ch.signature(ch_signature_type) + self.encryption_ch.signature(ch_signature_type)

    def _to_dict(self):
        dict_ch = {
            "key_schedule_ch": self.key_schedule_ch._to_dict(),
            "encryption_ch": self.encryption_ch._to_dict(),
        }
        return dict_ch

    def __str__(self):
        return pprint.pformat(self._to_dict(), width=100, compact=True)
arxpy/bitvector/operation.py
class RotateLeft(Operation):
    """Circular left rotation operation.

        >>> from arxpy.bitvector.core import Constant, Variable
        >>> from arxpy.bitvector.operation import RotateLeft
        >>> RotateLeft(Constant(150, 8), 2)
        0x5a
        >>> RotateLeft(Variable("x", 8), 2)
        x <<< 2

    """

    arity = [1, 1]
    is_symmetric = False
    infix_symbol = "<<<"
    operand_types = [core.Term, int]

    @classmethod
    def condition(cls, x, r):
        return x.width > r >= 0

    @classmethod
    def output_width(cls, x, r):
        return x.width

    @classmethod
    def eval(cls, x, r):
        def doit(val, r, width):
            """Left cyclic rotation operation when both operands are int."""
            mask = 2 ** width - 1
            r = r % width
            return ((val << r) & mask) | ((val & mask) >> (width - r))

        if isinstance(x, core.Constant):
            return core.Constant(doit(int(x), r, x.width), x.width)
        elif r == 0:
            return x
        elif isinstance(x, RotateLeft):
            return RotateLeft(x.args[0], (x.args[1] + r) % x.args[0].width)
        elif isinstance(x, RotateRight):
            return RotateRight(x.args[0], (x.args[1] - r) % x.args[0].width)
arxpy/bitvector/operation.py
class RotateRight(Operation):
    """Circular right rotation operation.

    It provides Automatic Constant Conversion. See `Operation` for more
    information.

        >>> from arxpy.bitvector.core import Constant, Variable
        >>> from arxpy.bitvector.operation import RotateRight
        >>> RotateRight(Constant(150, 8), 3)
        0xd2
        >>> RotateRight(Variable("x", 8), 3)
        x >>> 3

    """

    arity = [1, 1]
    is_symmetric = False
    infix_symbol = ">>>"
    operand_types = [core.Term, int]

    @classmethod
    def condition(cls, x, r):
        return x.width > r >= 0

    @classmethod
    def output_width(cls, x, r):
        return x.width

    @classmethod
    def eval(cls, x, r):
        def doit(val, r, width):
            """Right cyclic rotation operation when both operands are int."""
            mask = 2 ** width - 1
            r = r % width
            return ((val & mask) >> r) | (val << (width - r) & mask)

        if isinstance(x, core.Constant):
            return core.Constant(doit(int(x), r, x.width), x.width)
        elif r == 0:
            return x
        elif isinstance(x, RotateRight):
            return RotateRight(x.args[0], (x.args[1] + r) % x.args[0].width)
        elif isinstance(x, RotateLeft):
            return RotateLeft(x.args[0], (x.args[1] - r) % x.args[0].width)
arxpy/differential/difference.py
class XorDiff(Difference):
    """Represent XOR differences.

    The XOR difference of two `Term` is given by the XOR
    of the terms. In other words, the *difference operation*
    of `XorDiff` is the `BvXor` (see `Difference`).

        >>> from arxpy.bitvector.core import Constant, Variable
        >>> from arxpy.differential.difference import XorDiff
        >>> x, y = Constant(0b000, 3), Constant(0b000, 3)
        >>> alpha = XorDiff.from_pair(x, y)
        >>> alpha
        XorDiff(0b000)
        >>> alpha.get_pair_element(x)
        0b000
        >>> x, y = Constant(0b010, 3), Constant(0b101, 3)
        >>> alpha = XorDiff.from_pair(x, y)
        >>> alpha
        XorDiff(0b111)
        >>> alpha.get_pair_element(x)
        0b101
        >>> k = Variable("k", 8)
        >>> alpha = XorDiff.from_pair(k, k)
        >>> alpha
        XorDiff(0x00)
        >>> alpha.get_pair_element(k)
        k
    """

    diff_op = operation.BvXor
    inv_diff_op = operation.BvXor

    @classmethod
    def derivative(cls, op, input_diff):
        """Return the derivative of ``op`` at the point ``input_diff``.

        See `Difference.derivative` for more information.

            >>> from arxpy.bitvector.core import Variable, Constant
            >>> from arxpy.bitvector.operation import BvAdd, BvXor, RotateLeft, BvSub
            >>> from arxpy.bitvector.extraop import make_partial_operation
            >>> from arxpy.differential.difference import XorDiff
            >>> d1, d2 = XorDiff(Variable("d1", 8)), XorDiff(Variable("d2", 8))
            >>> XorDiff.derivative(BvXor, [d1, d2])
            XorDiff(d1 ^ d2)
            >>> Xor1 = make_partial_operation(BvXor, tuple([None, Constant(1, 8)]))
            >>> XorDiff.derivative(Xor1, d1)
            XorDiff(d1)
            >>> Rotate1 = make_partial_operation(RotateLeft, tuple([None, 1]))
            >>> XorDiff.derivative(Rotate1, d1)
            XorDiff(d1 <<< 1)
            >>> XorDiff.derivative(BvAdd, [d1, d2])
            XDA(XorDiff(d1), XorDiff(d2))
            >>> XorDiff.derivative(BvSub, [d1, d2])
            XDS(XorDiff(d1), XorDiff(d2))
            >>> CteAdd1 = make_partial_operation(BvAdd, tuple([None, Constant(1, 8)]))
            >>> XorDiff.derivative(CteAdd1, d1)
            XDCA_0x01(XorDiff(d1))

        """
        input_diff = _tuplify(input_diff)
        assert len(input_diff) == sum(op.arity)

        msg = "invalid arguments: op={}, input_diff={}".format(
            op.__name__,
            [d.vrepr() if isinstance(d, core.Term) else d for d in input_diff])

        if not all(isinstance(diff, cls) for diff in input_diff):
            raise ValueError(msg)

        if op == operation.BvNot:
            return input_diff[0]

        if op == operation.BvXor:
            return cls(op(*[d.val for d in input_diff]))

        if op == operation.Concat:
            return cls(op(*[d.val for d in input_diff]))

        if op == operation.BvAdd:
            from arxpy.differential import derivative
            return derivative.XDA(input_diff)

        if op == operation.BvSub:
            from arxpy.differential import derivative
            return derivative.XDS(input_diff)

        if issubclass(op, extraop.PartialOperation):
            if op.base_op == operation.BvXor:
                assert len(input_diff) == 1
                d1 = input_diff[0]
                val = op.fixed_args[0] if op.fixed_args[0] is not None else op.fixed_args[1]
                d2 = cls.from_pair(val, val)
                input_diff = [d1, d2]
                return cls(op.base_op(*[d.val for d in input_diff]))

            if op.base_op == operation.BvAnd:
                assert len(input_diff) == 1
                d1 = input_diff[0]
                val = op.fixed_args[0] if op.fixed_args[0] is not None else op.fixed_args[1]
                if isinstance(val, core.Constant):
                    return cls(op.base_op(d1.val, val))

            if op.base_op in [operation.RotateLeft, operation.RotateRight]:
                if op.fixed_args[0] is None and op.fixed_args[1] is not None:
                    assert len(input_diff) == 1
                    d = input_diff[0]
                    return cls(op.base_op(d.val, op.fixed_args[1]))
                else:
                    raise ValueError(msg)

            if op.base_op in [operation.BvShl, operation.BvLshr]:
                if op.fixed_args[0] is None and op.fixed_args[1] is not None:
                    assert len(input_diff) == 1
                    d = input_diff[0]
                    return cls(op.base_op(d.val, op.fixed_args[1]))
                else:
                    raise ValueError(msg)

            if op.base_op == operation.Extract:
                if op.fixed_args[0] is None and op.fixed_args[1] is not None and op.fixed_args[2] is not None:
                    assert len(input_diff) == 1
                    d = input_diff[0]
                    return cls(op.base_op(d.val, op.fixed_args[1], op.fixed_args[2]))
                else:
                    raise ValueError(msg)

            if op.base_op == operation.Concat:
                assert len(input_diff) == 1
                d1 = input_diff[0]
                if op.fixed_args[0] is not None:
                    val = op.fixed_args[0]
                    input_diff = [cls.from_pair(val, val), d1]
                else:
                    val = op.fixed_args[1]
                    input_diff = [d1, cls.from_pair(val, val)]
                return cls(op.base_op(*[d.val for d in input_diff]))

            if op.base_op == operation.BvAdd:
                assert len(input_diff) == 1
                d = input_diff[0]
                cte = op.fixed_args[0] if op.fixed_args[0] is not None else op.fixed_args[1]
                from arxpy.differential import derivative
                return derivative.XDCA(d, cte)
            else:
                raise ValueError(msg)

        if hasattr(op, "xor_derivative"):
            return op.xor_derivative(input_diff)

        raise ValueError(msg)
arxpy/bitvector/core.py
class Constant(basic.Atom, Term):
    """Represent bit-vector constants.

    Bit-vector constants are interpreted as unsigned integers in base 2,
    that is, a bit-vector :math:`(x_{n-1}, \dots, x_1, x_0)` represents
    the non-negative integer :math:`x_0 + 2 x_1 + \dots + 2^{n-1} x_{n-1}`.

    Args:
        val: the integer value.
        width: the bit-width.

    ::

        >>> from arxpy.bitvector.core import Constant
        >>> Constant(3, 12)
        0x003
        >>> Constant(0b11, 12)
        0x003
        >>> Constant(0x003, 12)
        0x003
        >>> Constant(3, 12).vrepr()
        'Constant(0b000000000011, width=12)'

    """

    def __int__(self):
        return self.val

    def __hash__(self):
        return super().__hash__()

    def __eq__(self, other):
        """Override == operator."""
        if isinstance(other, int):
            return self.val == other
        elif isinstance(other, Constant) and self.width == other.width:
            return self.val == other.val
        else:
            return False

    # def __index__(self):
    #     """Return an int to be used inside a slice [ : : ]."""
    #     return self.int

    def _hashable_content(self):
        """Return a tuple of information about self to compute its hash."""
        return self.val, self.width

    @classmethod
    def class_key(cls):
        """Return the key (identifier) of the class for sorting."""
        return 1, 0, cls.__name__

    # end Integer

    __slots__ = ["_val"]

    def __new__(cls, val, width):
        assert isinstance(val, int) and 0 <= val < 2 ** width
        obj = Term.__new__(cls, width=width)
        obj._val = val
        return obj

    def __bool__(self):
        if self.width == 1:
            return self == Constant(1, 1)
        else:
            raise AttributeError("only 1-bit constants implement bool()")

    @property
    def val(self):
        """The integer represented by the bit-vector constant."""
        return self._val

    @property
    def formula_size(self):
        """The formula size of the constant."""
        def log2(n):
            return int(math.ceil(math.log(n, 2)))

        def bin_enc(n):
            return 1 + log2(n + 1)

        return 1 + log2(int(self) + 1) + bin_enc(self.width)

    def bin(self):
        """Return the binary representation.

            >>> from arxpy.bitvector.core import Constant
            >>> print(Constant(3, 4).bin())
            0b0011
            >>> print(Constant(4, 6).bin())
            0b000100

        """
        width = self.width + 2  # 2 due to '0b'
        return format(self.val, r'0=#{}b'.format(width))

    def hex(self):
        """Return the hexadecimal representation.

            >>> from arxpy.bitvector.core import Constant
            >>> print(Constant(3, 4).hex())
            0x3

        """
        assert self.width % 4 == 0
        width = (self.width // 4) + 2
        return format(self.val, '0=#{}x'.format(width))

    def oct(self):
        """Return the octal representation.

            >>> from arxpy.bitvector.core import Constant
            >>> print(Constant(4, 6).oct())
            0o04

        """
        assert self.width % 3 == 0
        width = (self.width // 3) + 2
        return format(self.val, '0=#{}o'.format(width))
arxpy/differential/characteristic.py
class BvCharacteristic(object):
    """Represent characteristics of bit-vector functions.

    Given a bit-vector function `BvFunction` :math:`f`,
    a characteristic is a trail of differences obtained by
    propagating an input difference over :math:`f`.

    In particular, a characteristic is composed of the
    input difference and the output difference of each
    non-linear operation.

    This class manages symbolic characteristics,
    where the input difference is given symbolically
    and the intermediate differences are `Term`
    that depend on the input difference.

        >>> from arxpy.bitvector.core import Variable
        >>> from arxpy.differential.difference import XorDiff, RXDiff
        >>> from arxpy.differential.characteristic import BvCharacteristic
        >>> from arxpy.primitives.primitives import BvFunction
        >>> from arxpy.primitives.chaskey import ChaskeyPi
        >>> issubclass(ChaskeyPi, BvFunction)
        True
        >>> ChaskeyPi.set_rounds(1)
        >>> ch = BvCharacteristic(ChaskeyPi, XorDiff, ["dv0", "dv1", "dv2", "dv3"])
        >>> ch.ssa  # doctest: +NORMALIZE_WHITESPACE
        {'input_vars': (dv0, dv1, dv2, dv3),
        'output_vars': (d7, d12, d13, d9),
        'assignments': ((d0, dv0 + dv1), (d1, dv1 <<< 5), (d2, d0 ^ d1), (d3, d0 <<< 16), (d4, dv2 + dv3),
        (d5, dv3 <<< 8), (d6, d4 ^ d5), (d7, d3 + d6), (d8, d6 <<< 13), (d9, d7 ^ d8), (d10, d2 + d4),
        (d11, d2 <<< 7), (d12, d10 ^ d11), (d13, d10 <<< 16))}
        >>> ch.input_diff
        (XorDiff(dv0), XorDiff(dv1), XorDiff(dv2), XorDiff(dv3))
        >>> ch.nonlinear_diffs # doctest: +NORMALIZE_WHITESPACE
        OrderedDict([(XorDiff(d0), XDA(XorDiff(dv0), XorDiff(dv1))),
        (XorDiff(d4), XDA(XorDiff(dv2), XorDiff(dv3))),
        (XorDiff(d7), XDA(XorDiff(d0 <<< 16), XorDiff(d4 ^ (dv3 <<< 8)))),
        (XorDiff(d10), XDA(XorDiff(d0 ^ (dv1 <<< 5)), XorDiff(d4)))])
        >>> ch.output_diff # doctest: +NORMALIZE_WHITESPACE
        [[XorDiff(d7), XorDiff(d7)],
        [XorDiff(d12), XorDiff(d10 ^ ((d0 ^ (dv1 <<< 5)) <<< 7))],
        [XorDiff(d13), XorDiff(d10 <<< 16)],
        [XorDiff(d9), XorDiff(d7 ^ ((d4 ^ (dv3 <<< 8)) <<< 13))]]
        >>> ch = BvCharacteristic(ChaskeyPi, RXDiff, ["dv0", "dv1", "dv2", "dv3"])
        >>> ch.input_diff
        (RXDiff(dv0), RXDiff(dv1), RXDiff(dv2), RXDiff(dv3))
        >>> ch.nonlinear_diffs # doctest: +NORMALIZE_WHITESPACE
        OrderedDict([(RXDiff(d0), RXDA(RXDiff(dv0), RXDiff(dv1))),
        (RXDiff(d4), RXDA(RXDiff(dv2), RXDiff(dv3))),
        (RXDiff(d7), RXDA(RXDiff(d0 <<< 16), RXDiff(d4 ^ (dv3 <<< 8)))),
        (RXDiff(d10), RXDA(RXDiff(d0 ^ (dv1 <<< 5)), RXDiff(d4)))])
        >>> ch.output_diff # doctest: +NORMALIZE_WHITESPACE
        [[RXDiff(d7), RXDiff(d7)],
        [RXDiff(d12), RXDiff(d10 ^ ((d0 ^ (dv1 <<< 5)) <<< 7))],
        [RXDiff(d13), RXDiff(d10 <<< 16)],
        [RXDiff(d9), RXDiff(d7 ^ ((d4 ^ (dv3 <<< 8)) <<< 13))]]

    Attributes:
        func: the `BvFunction`
        diff_type: the `Difference` of the characteristic
        input_diff: a list containing the input symbolic differences
        nonlinear_diffs: an `collections.OrderedDict` mapping non-linear symbolic differences
            to their corresponding `Derivative`
        output_diff: a list, where the i-th element is a pair containing
            the i-th output symbolic difference and its value
    """

    def __init__(self, func, diff_type, input_diff_names, prefix="d", initial_var2diff=None):
        assert issubclass(func, primitives.BvFunction)
        assert issubclass(diff_type, difference.Difference)

        assert len(input_diff_names) == len(func.input_widths)
        input_diff = []
        for name, width in zip(input_diff_names, func.input_widths):
            input_diff.append(diff_type(core.Variable(name, width)))
        input_diff = tuple(input_diff)

        self.func = func
        self.diff_type = diff_type
        self.input_diff = input_diff

        # Propagate the input difference through the function

        names = [d.val.name for d in self.input_diff]
        ssa = self.func.ssa(names, id_prefix=prefix)
        self.ssa = ssa
        self._prefix = prefix
        self._input_diff_names = input_diff_names

        for var in ssa["output_vars"]:
            if isinstance(var, core.Constant):
                raise ValueError("constant outputs (independent of the inputs) are not supported")

        var2diff = {}  # Variable to Difference
        for var, diff in zip(ssa["input_vars"], self.input_diff):
            var2diff[var] = diff

        if initial_var2diff is not None:
            for var in initial_var2diff:
                if str(var) in names:
                    raise ValueError("the input differences cannot be replaced by initial_var2diff")
            var2diff.update(initial_var2diff)

        self.nonlinear_diffs = collections.OrderedDict()
        for var, expr in ssa["assignments"]:
            expr_args = []
            for arg in expr.args:
                if isinstance(arg, int):
                    expr_args.append(arg)  # 'int' object has no attribute 'xreplace'
                else:
                    expr_args.append(arg.xreplace(var2diff))

            if all(not isinstance(arg, diff_type) for arg in expr_args):
                # symbolic computations with the key
                var2diff[var] = expr
                continue

            if all(isinstance(arg, diff_type) for arg in expr_args):
                der = self.diff_type.derivative(type(expr), expr_args)
            else:
                def contains_key_var(term):
                    from sympy import basic
                    for sub in basic.preorder_traversal(term):
                        if sub in func.round_keys:
                            return True
                    else:
                        return False

                if type(expr) == operation.BvAdd and hasattr(func, 'round_keys') and \
                        all(isinstance(r, core.Variable) for r in func.round_keys) and \
                        any(contains_key_var(a) for a in expr_args):
                    # temporary solution to Derivative(BvAddCte_k(x)) != Derivative(x + k)
                    # with x a Diff and k a key variable
                    keyed_indices = []
                    for i, a in enumerate(expr_args):
                        if contains_key_var(a):
                            keyed_indices.append(i)
                    if len(keyed_indices) != 1 or expr_args[keyed_indices[0]] not in func.round_keys:
                        raise NotImplementedError("invalid expression: op={}, args={}".format(
                            type(expr).__name__, expr_args))
                    # expr_args[keyed_indices[0]] replaced to the zero diff
                    zero_diff = diff_type(core.Constant(0, expr_args[keyed_indices[0]].width))
                    der = self.diff_type.derivative(type(expr), [expr_args[(keyed_indices[0] + 1) % 2], zero_diff])
                elif hasattr(expr, "xor_derivative"):
                    # temporary solution to operations containing a custom derivative
                    input_diff_expr = []
                    for i, arg in enumerate(expr_args):
                        if isinstance(arg, diff_type):
                            input_diff_expr.append(arg)
                        else:
                            assert isinstance(arg, core.Term)  # int arguments currently not supported
                            input_diff_expr.append(diff_type.from_pair(arg, arg))
                    der = self.diff_type.derivative(type(expr), input_diff_expr)
                else:
                    fixed_args = []
                    for i, arg in enumerate(expr_args):
                        if not isinstance(arg, diff_type):
                            fixed_args.append(arg)
                        else:
                            fixed_args.append(None)
                    new_op = extraop.make_partial_operation(type(expr), tuple(fixed_args))
                    der = self.diff_type.derivative(new_op, [arg for arg in expr_args if isinstance(arg, diff_type)])

            if isinstance(der, derivative.Derivative):
                diff = self.diff_type(var)
                var2diff[var] = diff
                self.nonlinear_diffs[diff] = der
            else:
                var2diff[var] = der

        self._var2diff = var2diff

        self.output_diff = []
        for var in ssa["output_vars"]:
            self.output_diff.append([self.diff_type(var), var2diff[var]])

    def empirical_weight(self, input_diff, output_diff, pair_samples):
        """Return the empirical weight of a given differential.

        Given a differential (a pair of input and output differences),
        the differential probability is the fraction of input pairs
        with the given input difference leading to output pairs
        with the given output difference.

        This method returns an approximation of the weight of the
        differential probability by sampling a given number
        of input pairs.

        If no correct output pairs are found, `math.inf` is returned.

            >>> from arxpy.bitvector.core import Constant
            >>> from arxpy.differential.difference import XorDiff, RXDiff
            >>> from arxpy.differential.characteristic import BvCharacteristic
            >>> from arxpy.primitives.chaskey import ChaskeyPi
            >>> ChaskeyPi.set_rounds(1)
            >>> ch = BvCharacteristic(ChaskeyPi, XorDiff, ["dv" + str(i) for i in range(4)])
            >>> zero, one = XorDiff(Constant(0, 32)), XorDiff(Constant(1, 32))
            >>> ch.empirical_weight([zero, zero, zero, zero], [zero, zero, zero, zero], 100)
            0.0
            >>> ch.empirical_weight([zero, zero, zero, zero], [one, one, one, one], 100)
            inf
            >>> ch = BvCharacteristic(ChaskeyPi, RXDiff, ["dv" + str(i) for i in range(4)])
            >>> zero, one = RXDiff(Constant(0, 32)), RXDiff(Constant(1, 32))
            >>> 4 - 1 <= ch.empirical_weight([zero]*4, [zero]*4, 3 * 2**6) <= 8
            True
            >>> ch.empirical_weight([zero]*4, [one]*4, 3 * 2**6)
            inf

        """
        assert isinstance(input_diff, collections.abc.Sequence)
        assert isinstance(output_diff, collections.abc.Sequence)
        assert all(isinstance(d, difference.Difference) for d in input_diff)
        assert all(isinstance(d, difference.Difference) for d in output_diff)
        assert all(isinstance(d.val, core.Constant) for d in input_diff)
        assert all(isinstance(d.val, core.Constant) for d in output_diff)

        assert len(input_diff) == len(self.input_diff)
        assert len(output_diff) == len(self.output_diff)
        assert len(self.ssa["input_vars"]) == len(input_diff)
        assert len(self.ssa["output_vars"]) == len(output_diff)

        with context.Simplification(False):
            input_widths = [d.val.width for d in self.input_diff]
            if pair_samples >= 2**sum(input_widths):
                iterators = [range(2 ** w) for w in input_widths]
                list_pairs = []
                for x in itertools.product(*iterators):
                    pt = [core.Constant(x_i, w) for x_i, w in zip(x, input_widths)]
                    other_pt = [diff.get_pair_element(pt[i]) for i, diff in enumerate(input_diff)]
                    list_pairs.append([pt, other_pt])
                pair_samples = len(list_pairs)
                assert pair_samples == 2**sum(input_widths)
            else:
                list_pairs = []
                for _ in range(pair_samples):
                    pt = []
                    other_pt = []
                    for diff in input_diff:
                        random_int = random.randrange(2 ** diff.val.width)
                        random_bv = core.Constant(random_int, diff.val.width)
                        pt.append(random_bv)
                        other_pt.append(diff.get_pair_element(random_bv))
                    list_pairs.append([pt, other_pt])

            correct_pairs = 0

            for index_input in range(pair_samples):
                pt, other_pt = list_pairs[index_input]
                ct = self.func(*pt)
                other_ct = self.func(*other_pt)

                assert all(isinstance(x, core.Constant) for x in ct), str(ct)
                assert all(isinstance(x, core.Constant) for x in other_ct), str(other_ct)

                for i, diff in enumerate(output_diff):
                    # noinspection PyUnresolvedReferences
                    if self.diff_type.from_pair(ct[i], other_ct[i]) != diff:
                        break
                else:
                    correct_pairs += 1

            if correct_pairs == 0:
                weight = math.inf
            else:
                weight = abs(-math.log(correct_pairs * 1.0 / pair_samples, 2))

        return weight

    def _empirical_weight_distribution(self, cipher, input_diff, output_diff, pair_samples, key_samples,
                                       precision=1, rk_output_diff=None):
        # this function is not part of SingleKeyCh since it must be accessible
        # for the encryption characteristic of RelatedKeyCh (which is a
        # plain BvCharacteristic)
        assert isinstance(input_diff, collections.abc.Sequence)
        assert isinstance(output_diff, collections.abc.Sequence)
        assert all(isinstance(d, difference.Difference) for d in input_diff)
        assert all(isinstance(d, difference.Difference) for d in output_diff)
        assert all(isinstance(d.val, core.Constant) for d in input_diff)
        assert all(isinstance(d.val, core.Constant) for d in output_diff)

        assert len(input_diff) == len(self.input_diff)
        assert len(output_diff) == len(self.output_diff)
        assert len(self.ssa["input_vars"]) == len(input_diff)
        assert len(self.ssa["output_vars"]) == len(output_diff)

        old_round_keys = self.func.round_keys

        empirical_weights = collections.Counter()

        if rk_output_diff is not None:
            class RelatedFunc(self.func):
                pass
        else:
            RelatedFunc = self.func

        with context.Simplification(False):
            input_widths = [d.val.width for d in self.input_diff]
            if pair_samples >= 2**sum(input_widths):
                iterators = [range(2 ** w) for w in input_widths]
                list_pairs = []
                for x in itertools.product(*iterators):
                    pt = [core.Constant(x_i, w) for x_i, w in zip(x, input_widths)]
                    other_pt = [diff.get_pair_element(pt[i]) for i, diff in enumerate(input_diff)]
                    list_pairs.append([pt, other_pt])
                pair_samples = len(list_pairs)
                assert pair_samples == 2**sum(input_widths)
            else:
                list_pairs = []
                for _ in range(pair_samples):
                    pt = []
                    other_pt = []
                    for diff in input_diff:
                        random_int = random.randrange(2 ** diff.val.width)
                        random_bv = core.Constant(random_int, diff.val.width)
                        pt.append(random_bv)
                        other_pt.append(diff.get_pair_element(random_bv))
                    list_pairs.append([pt, other_pt])

            for _ in range(key_samples):
                master_key = []
                for width in cipher.key_schedule.input_widths:
                    master_key.append(core.Constant(random.randrange(2 ** width), width))
                self.func.round_keys = cipher.key_schedule(*master_key)
                assert all(isinstance(rk, core.Constant) for rk in self.func.round_keys), str(self.func.round_keys)

                if rk_output_diff is not None:
                    RelatedFunc.round_keys = [d.get_pair_element(r) for r, d in zip(self.func.round_keys, rk_output_diff)]
                    assert all(isinstance(rk, core.Constant) for rk in RelatedFunc.round_keys), str(RelatedFunc.round_keys)

                correct_pairs = 0

                for index_input in range(pair_samples):
                    pt, other_pt = list_pairs[index_input]
                    ct = self.func(*pt)
                    other_ct = RelatedFunc(*other_pt)

                    assert all(isinstance(x, core.Constant) for x in ct), str(ct)
                    assert all(isinstance(x, core.Constant) for x in other_ct), str(other_ct)

                    for i, diff in enumerate(output_diff):
                        # noinspection PyUnresolvedReferences
                        if self.diff_type.from_pair(ct[i], other_ct[i]) != diff:
                            break
                    else:
                        correct_pairs += 1

                if correct_pairs == 0:
                    weight = math.inf
                else:
                    weight = abs(-math.log(correct_pairs * 1.0 / pair_samples, 2))
                # weight = float(("{0:."+str(precision)+"f}").format(weight))
                weight = round(weight, precision)
                empirical_weights[weight] += 1

        self.func.round_keys = old_round_keys

        return empirical_weights

    def signature(self, ch_signature_type):
        """Return the signature of the characteristic.

        The signature is a "hash" of the characteristic used for comparing.

        For the type of the signature, see `ChSignatureType`.

            >>> from arxpy.bitvector.core import Variable
            >>> from arxpy.differential.difference import XorDiff
            >>> from arxpy.differential.characteristic import BvCharacteristic, ChSignatureType
            >>> from arxpy.primitives.primitives import BvFunction
            >>> from arxpy.primitives.chaskey import ChaskeyPi
            >>> issubclass(ChaskeyPi, BvFunction)
            True
            >>> ChaskeyPi.set_rounds(1)
            >>> ch = BvCharacteristic(ChaskeyPi, XorDiff, ["dv0", "dv1", "dv2", "dv3"])
            >>> ch.signature(ChSignatureType.Full)
            [dv0, dv1, dv2, dv3, d0, d4, d7, d10]
            >>> ch.signature(ChSignatureType.InputOutput)
            [dv0, dv1, dv2, dv3, d7, d12, d13, d9]

        """
        if ch_signature_type == ChSignatureType.Full:
            return [d.val for d in self.input_diff] + [d.val for d in self.nonlinear_diffs]
        elif ch_signature_type == ChSignatureType.InputOutput:
            # sig = [d for d in self.input_diff]
            sig_var = [d.val for d in self.input_diff]

            for out_diff, _ in self.output_diff:
                for aux_var in self._var2diff[out_diff.val].val.atoms(core.Variable):
                    if aux_var not in sig_var:
                        # sig.append(out_diff)
                        sig_var.append(out_diff.val)
                        break

            return sig_var
        else:
            raise ValueError("invalid ch_signature_type: {}".format(ch_signature_type))

    def _to_dict(self):
        dict_ch = {
            "ssa": self.ssa,
            "input_diff": self.input_diff,
            "output_diff": self.output_diff,
            "nonlinear_diffs": self.nonlinear_diffs,
        }
        return dict_ch

    def __str__(self):
        return pprint.pformat(self._to_dict(), width=100, compact=True)
arxpy/primitives/primitives.py
class Encryption(BvFunction):
    """Represent encryption functions.

    An encryption function is a `BvFunction` that takes
    the plaintext as input and returns the ciphertext
    for some fixed key.
    See `BvFunction` for more information.

    Attributes:
        round_keys: a list of `Term` representing the round keys

    """
    round_keys = None
arxpy/bitvector/core.py
class Variable(basic.Atom, Term):
    """Represent bit-vector variables.

    Args:
        name: the name of the variable.
        width: the bit-width.

    ::

        >>> from arxpy.bitvector.core import Variable
        >>> Variable("x", 12)
        x
        >>> Variable("x", 12).vrepr()
        "Variable('x', width=12)"

    """

    def _hashable_content(self):
        """Return a tuple of information about self to compute hash."""
        return self.name, self.width

    # def __call__(self, *args):
    #     from sympy.core.function as function
    #     return function.UndefinedFunction(self.name, self.width)(*args)

    # end Symbol

    __slots__ = ['_name']

    def __new__(cls, name, width):
        assert isinstance(name, str)
        obj = Term.__new__(cls, width=width)
        obj._name = name

        return obj

    @property
    def name(self):
        """The name of the variable."""
        return self._name

    @property
    def formula_size(self):
        """The formula size of the variable."""
        def log2(n):
            return int(math.ceil(math.log(n, 2)))

        def bin_enc(n):
            return 1 + log2(n + 1)

        return 1 + bin_enc(self.width)
arxpy/differential/characteristic.py
class SingleKeyCh(BvCharacteristic):
    """Represent single-key characteristics of block ciphers.

    A single-key characteristic of a `Cipher` is a `BvCharacteristic`
    over the `Encryption` function of the cipher.

    The plaintext differences start with the prefix ``"dp"``
    and the non-linear differences start with the prefix ``"dx"``.

        >>> from arxpy.bitvector.core import Variable
        >>> from arxpy.differential.difference import XorDiff
        >>> from arxpy.differential.characteristic import SingleKeyCh
        >>> from arxpy.primitives.primitives import Cipher
        >>> from arxpy.primitives import speck
        >>> Speck32 = speck.get_Speck_instance(speck.SpeckInstance.speck_32_64)
        >>> issubclass(Speck32, Cipher)
        True
        >>> Speck32.set_rounds(1)
        >>> ch = SingleKeyCh(Speck32, XorDiff)
        >>> ch .ssa  # doctest: +NORMALIZE_WHITESPACE
        {'input_vars': (dp0, dp1), 'output_vars': (dx2, dx4),
        'assignments': ((dx0, dp0 >>> 7), (dx1, dp1 + dx0), (dx2, dx1 ^ k0), (dx3, dp1 <<< 2), (dx4, dx2 ^ dx3))}
        >>> ch.input_diff
        (XorDiff(dp0), XorDiff(dp1))
        >>> ch.nonlinear_diffs
        OrderedDict([(XorDiff(dx1), XDA(XorDiff(dp1), XorDiff(dp0 >>> 7)))])
        >>> ch.output_diff
        [[XorDiff(dx2), XorDiff(dx1)], [XorDiff(dx4), XorDiff(dx1 ^ (dp1 <<< 2))]]

    """

    def __init__(self, bv_cipher, diff_type):
        assert issubclass(bv_cipher, primitives.Cipher)
        assert issubclass(diff_type, difference.Difference)

        rk = []
        for i, width in enumerate(bv_cipher.key_schedule.output_widths):
            rk.append(core.Variable("k" + str(i), width))

        class Encryption(bv_cipher.encryption):
            round_keys = tuple(rk)

        func = Encryption
        num_inputs = len(func.input_widths)
        input_diff_names = ["dp" + str(i) for i in range(num_inputs)]
        prefix = "dx"
        super().__init__(func, diff_type, input_diff_names, prefix)
        self._cipher = bv_cipher

    def empirical_weight(self, input_diff, output_diff, pair_samples, key_samples,
                         precision=1, rk_diffs=None):
        """Return the empirical weight distribution of a given differential.

        This method returns a `collections.Counter` storing the distribution of
        differential probability weights over the given number of keys.

        The weights are rounded to the given number of precision
        digits after the decimal point.

        See also `BvCharacteristic.empirical_weight`.

            >>> from arxpy.bitvector.core import Constant
            >>> from arxpy.differential.difference import XorDiff
            >>> from arxpy.differential.characteristic import SingleKeyCh
            >>> from arxpy.primitives import speck
            >>> Speck32 = speck.get_Speck_instance(speck.SpeckInstance.speck_32_64)
            >>> Speck32.set_rounds(1)
            >>> ch = SingleKeyCh(Speck32, XorDiff)
            >>> zero, one = XorDiff(Constant(0, 16)), XorDiff(Constant(1, 16))
            >>> ch.empirical_weight([zero, zero], [zero, zero], 100, 10)
            Counter({0.0: 10})
            >>> ch.empirical_weight([zero, zero], [one, one], 100, 10)
            Counter({inf: 10})

        """
        return self._empirical_weight_distribution(self._cipher, input_diff, output_diff, pair_samples, key_samples,
                                                   precision, rk_diffs)
arxpy/differential/characteristic.py
class ChSignatureType(enum.Enum):
class BvCharacteristic(object):
            class RelatedFunc(self.func):
class SingleKeyCh(BvCharacteristic):
        class Encryption(bv_cipher.encryption):
class RelatedKeyCh(object):
        class Encryption(bv_cipher.encryption):
    def __init__(self, func, diff_type, input_diff_names, prefix="d", initial_var2diff=None):
                def contains_key_var(term):
    def empirical_weight(self, input_diff, output_diff, pair_samples):
    def _empirical_weight_distribution(self, cipher, input_diff, output_diff, pair_samples, key_samples,
                                       precision=1, rk_output_diff=None):
    def signature(self, ch_signature_type):
    def _to_dict(self):
    def __str__(self):
    def __init__(self, bv_cipher, diff_type):
    def empirical_weight(self, input_diff, output_diff, pair_samples, key_samples,
                         precision=1, rk_diffs=None):
    def __init__(self, bv_cipher, diff_type):
    def empirical_weight(self, key_input_diff, key_output_diff, key_samples,
                         enc_input_diff, enc_output_diff, enc_samples, precision=1):
    def signature(self, ch_signature_type):
    def _to_dict(self):
    def __str__(self):
arxpy/differential/difference.py
class RXDiff(Difference):
    """Represent rotational-XOR (RX) differences.

    The pair ``(x, (x <<< 1) ^ d)`` has RX difference ``d``.
    In other words,  the RX difference of two `Term` ``x`` and ``y``
    is defined as ``(x <<< 1) ^ y``.

    See `Difference` for more information.

        >>> from arxpy.bitvector.core import Constant, Variable
        >>> from arxpy.differential.difference import RXDiff
        >>> x, y = Constant(0b000, 3), Constant(0b000, 3)
        >>> alpha = RXDiff.from_pair(x, y)
        >>> alpha
        RXDiff(0b000)
        >>> alpha.get_pair_element(x)
        0b000
        >>> x, y = Constant(0b000, 3), Constant(0b001, 3)
        >>> alpha = RXDiff.from_pair(x, y)
        >>> alpha
        RXDiff(0b001)
        >>> alpha.get_pair_element(x)
        0b001
        >>> k = Variable("k", 8)
        >>> alpha = RXDiff.from_pair(k, k)
        >>> alpha
        RXDiff(k ^ (k <<< 1))
        >>> alpha.get_pair_element(k)
        k
    """

    diff_op = RXOp
    inv_diff_op = RXInvOp

    @classmethod
    def derivative(cls, op, input_diff):
        """Return the derivative of ``op`` at the point ``input_diff``.

        See `Difference.derivative` for more information.

            >>> from arxpy.bitvector.core import Variable, Constant
            >>> from arxpy.bitvector.operation import BvAdd, BvXor, RotateLeft
            >>> from arxpy.bitvector.extraop import make_partial_operation
            >>> from arxpy.differential.difference import RXDiff
            >>> d1, d2 = RXDiff(Variable("d1", 8)), RXDiff(Variable("d2", 8))
            >>> RXDiff.derivative(BvXor, [d1, d2])
            RXDiff(d1 ^ d2)
            >>> Xor1 = make_partial_operation(BvXor, tuple([None, Constant(1, 8)]))
            >>> RXDiff.derivative(Xor1, d1)
            RXDiff(0x03 ^ d1)
            >>> Rotate1 = make_partial_operation(RotateLeft, tuple([None, 1]))
            >>> RXDiff.derivative(Rotate1, d1)
            RXDiff(d1 <<< 1)
            >>> RXDiff.derivative(BvAdd, [d1, d2])
            RXDA(RXDiff(d1), RXDiff(d2))

        """
        input_diff = _tuplify(input_diff)
        assert len(input_diff) == sum(op.arity)

        msg = "invalid arguments: op={}, input_diff={}".format(
            op.__name__,
            [d.vrepr() if isinstance(d, core.Term) else d for d in input_diff])

        if not all(isinstance(diff, cls) for diff in input_diff):
            raise ValueError(msg)

        if op == operation.BvNot:
            return input_diff[0]

        if op == operation.BvXor:
            return cls(op(*[d.val for d in input_diff]))

        if op == operation.BvAdd:
            from arxpy.differential import derivative
            return derivative.RXDA(input_diff)

        # Concact, BvSub

        if issubclass(op, extraop.PartialOperation):
            if op.base_op == operation.BvXor:
                assert len(input_diff) == 1
                d1 = input_diff[0]
                val = op.fixed_args[0] if op.fixed_args[0] is not None else op.fixed_args[1]
                d2 = cls.from_pair(val, val)
                input_diff = [d1, d2]
                return cls(op.base_op(*[d.val for d in input_diff]))

            if op.base_op in [operation.RotateLeft, operation.RotateRight]:
                if op.fixed_args[0] is None and op.fixed_args[1] is not None:
                    assert len(input_diff) == 1
                    d = input_diff[0]
                    return cls(op.base_op(d.val, op.fixed_args[1]))
                else:
                    raise ValueError(msg)

            # BvShl, Extract

        if hasattr(op, "rx_derivative"):
            return op.rx_derivative(input_diff)

        raise ValueError(msg)
arxpy/primitives/primitives.py
class BvFunction(object):
    """Represent (iterated) fixed-width bit-vector functions.

    A `BvFunction` takes fixed-width `Constant` operands and return a
    tuple of fixed-width `Constant`. An iterated bit-vector function
    contains a subroutine that is iterated a certain number of *rounds*,
    which can be changed using `set_rounds`.

    Similar to `Operation`, `BvFunction` is evaluated
    using the operator ``()`` and provides *Automatic Constant Conversion*.
    Note that `BvFunction` only accepts `Constant` operands and
    always return a tuple, as opposed to `Operation` that accepts
    `Term` and scalar operands and returns a single `Term`.

        >>> from arxpy.primitives.primitives import BvFunction
        >>> from arxpy.primitives.chaskey import ChaskeyPi
        >>> issubclass(ChaskeyPi, BvFunction)
        True
        >>> ChaskeyPi(0, 0, 0, 0)  # automatic conversion from int to Constant
        (0x00000000, 0x00000000, 0x00000000, 0x00000000)

    Attributes:
        input_widths: a list containing the widths of the inputs
        output_widths: a list containing the widths of the outputs
        rounds: the number of iterations

    """
    input_widths = None
    output_widths = None
    rounds = None

    def __new__(cls, *args, **options):
        if len(cls.input_widths) != len(args):
            raise ValueError("{} requires {} inputs but {} were given: {}".format(
                cls.__name__, len(cls.input_widths), len(args), args))
        newargs = []
        for arg, width in zip(args, cls.input_widths):
            newargs.append(core.bitvectify(arg, width))
        args = newargs

        if all(isinstance(arg, core.Constant) for arg in args) or \
                options.pop("symbolic_inputs", False):
            result = cls.eval(*args)
        else:
            raise TypeError("expected bit-vector constant arguments")

        assert isinstance(result, collections.abc.Sequence)
        assert len(cls.output_widths) == len(result)

        output = []
        for r, width in zip(result, cls.output_widths):
            output.append(core.bitvectify(r, width))

        return tuple(output)

    @classmethod
    def eval(cls, *args):
        """Evaluate the function (internal method)."""
        raise NotImplementedError("subclasses need to override this method")

    @classmethod
    def set_rounds(cls, new_rounds):
        """Change the number of rounds and adjust the input/output widths."""
        raise NotImplementedError("subclasses need to override this method")

    # noinspection PyArgumentList
    @classmethod
    def ssa(cls, input_names, id_prefix):
        """Return a static single assignment program representing the function.

        Args:
            input_names: the names  for the input variables
            id_prefix: the prefix to denote the intermediate variables

        Return:
            : a dictionary with three keys

            - *input_vars*: a list of `Variable` representing the inputs
            - *output_vars*: a list of `Variable` representing the outputs
            - *assignments*: an ordered sequence of pairs
              (`Variable`, `Operation`) representing each assignment
              of the SSA program.

        ::

                >>> from arxpy.primitives.chaskey import ChaskeyPi
                >>> ChaskeyPi.set_rounds(1)
                >>> ChaskeyPi.ssa(["v0", "v1", "v2", "v3"], "x")  # doctest: +NORMALIZE_WHITESPACE
                {'input_vars': (v0, v1, v2, v3),
                'output_vars': (x7, x12, x13, x9),
                'assignments': ((x0, v0 + v1), (x1, v1 <<< 5), (x2, x0 ^ x1), (x3, x0 <<< 16), (x4, v2 + v3),
                (x5, v3 <<< 8), (x6, x4 ^ x5), (x7, x3 + x6), (x8, x6 <<< 13), (x9, x7 ^ x8), (x10, x2 + x4),
                (x11, x2 <<< 7), (x12, x10 ^ x11), (x13, x10 <<< 16))}

        """
        input_vars = []
        for name, width in zip(input_names, cls.input_widths):
            input_vars.append(core.Variable(name, width))
        input_vars = tuple(input_vars)

        table = context.MemoizationTable(id_prefix=id_prefix)

        with context.Memoization(table):
            # noinspection PyArgumentList
            output_vars = cls(*input_vars, symbolic_inputs=True)

        ssa_dict = {
            "input_vars": input_vars,
            "output_vars": output_vars,
            "assignments": tuple(table.items())
        }

        for var, expr in ssa_dict["assignments"]:
            for arg in expr.args:
                if isinstance(arg, operation.Operation):
                    raise ValueError("assignment {} <- {} was not decomposed".format(var, expr))

        to_delete = []
        vars_needed = set()
        for var in output_vars:
            vars_needed.add(var)
        for var, expr in reversed(ssa_dict["assignments"]):
            if var in vars_needed:
                for arg in expr.atoms(core.Variable):
                    vars_needed.add(arg)
            else:
                to_delete.append((var, expr))
                # raise ValueError("assignment {} <- {} is redundant in \n{}".format(var, expr, ssa_dict))

        if len(to_delete) > 0:
            import warnings
            warnings.warn("removing redundant assignments {} in \n{}".format(to_delete, ssa_dict))
            ssa_dict["assignments"] = list(ssa_dict["assignments"])
            for assignment in to_delete:
                ssa_dict["assignments"].remove(assignment)
            ssa_dict["assignments"] = tuple(ssa_dict["assignments"])

        return ssa_dict
arxpy/primitives/primitives.py
class KeySchedule(BvFunction):
    """Represent key schedule functions.

    A key schedule function is a `BvFunction` that takes
    the masterkey as input and returns the round keys.
    See `BvFunction` for more information.
    """
arxpy/primitives/primitives.py
class Cipher(object):
    """Represent (iterated) block ciphers.

    A (iterated) block cipher consists of `KeySchedule` function
    that computes round keys from a master key and an `Encryption`
    function that computes a ciphertext from a given plaintext
    and the round keys.

    Given a ``cipher``, it can be evaluated with the operator ``()``
    by passing it as arguments the plaintext and the master key,
    that is, ``cipher(plaintext, masterkey)`` returns the ciphertext.

        >>> from arxpy.primitives.primitives import Cipher
        >>> from arxpy.primitives import speck
        >>> Speck32 = speck.get_Speck_instance(speck.SpeckInstance.speck_32_64)
        >>> issubclass(Speck32, Cipher)
        True
        >>> plaintext = [0, 0]
        >>> masterkey = [0, 0, 0, 0]
        >>> Speck32(plaintext, masterkey)
        (0x2bb9, 0xc642)

    Attributes:
        key_schedule: the `KeySchedule` function of the cipher
        encryption: the `Encryption` function of the cipher

    """
    key_schedule = None
    encryption = None
    rounds = None

    _minimum_rounds = 1  # for testing

    def __new__(cls, plaintext, masterkey, **options):
        assert isinstance(plaintext, collections.abc.Sequence)
        assert isinstance(masterkey, collections.abc.Sequence)
        assert cls.rounds >= cls._minimum_rounds

        previous_round_keys = cls.encryption.round_keys

        round_keys = cls.key_schedule(*masterkey, **options)
        cls.encryption.round_keys = round_keys
        result = cls.encryption(*plaintext, **options)

        cls.encryption.round_keys = previous_round_keys

        return result

    @classmethod
    def set_rounds(cls, new_rounds):
        """Change the number of rounds and adjust the input/output widths."""
        raise NotImplementedError("subclasses need to override this method")


Question: import doctest
import unittest
from hypothesis import given
from hypothesis.strategies import integers
from arxpy.bitvector.core import Variable, Constant
from arxpy.bitvector.operation import RotateLeft, RotateRight
from arxpy.primitives.primitives import BvFunction, KeySchedule, Encryption, Cipher
from arxpy.differential.difference import XorDiff, RXDiff
from arxpy.differential import characteristic
from arxpy.differential.characteristic import BvCharacteristic, SingleKeyCh, RelatedKeyCh
"""Tests for the Characteristic module."""






VERBOSE = False


# 1st cipher: linear key schedule and encryption with modular addition (no ctes)

class MyFunction(BvFunction):  # noqa: D101
    input_widths = [8, 8]
    output_widths = [8]
    rounds = 1

    @classmethod
    def eval(cls, x, y):
        x = RotateLeft(x ^ Constant(1, 8), 1)
        y = ~y
        return tuple([x + y])


class KeySchedule1(KeySchedule):  # noqa: D101
    input_widths = [8]
    output_widths = [8, 8]

    @classmethod
    def eval(cls, mk):
        return tuple([mk, mk ^ Constant(1, 8)])



Answer: class Encryption1(Encryption):  # noqa: D101