├── .github └── workflows │ └── pytest.yml ├── .gitignore ├── LICENSE ├── README.md ├── attacks ├── acd │ ├── mp.py │ ├── ol.py │ └── sda.py ├── cbc │ ├── bit_flipping.py │ ├── iv_recovery.py │ └── padding_oracle.py ├── cbc_and_cbc_mac │ ├── eam_key_reuse.py │ ├── etm_key_reuse.py │ └── mte_key_reuse.py ├── cbc_mac │ └── length_extension.py ├── ctr │ ├── bit_flipping.py │ ├── crime.py │ └── separator_oracle.py ├── ecb │ ├── plaintext_recovery.py │ ├── plaintext_recovery_harder.py │ └── plaintext_recovery_hardest.py ├── ecc │ ├── ecdsa_nonce_reuse.py │ ├── frey_ruck_attack.py │ ├── mov_attack.py │ ├── parameter_recovery.py │ ├── singular_curve.py │ └── smart_attack.py ├── elgamal_encryption │ ├── nonce_reuse.py │ └── unsafe_generator.py ├── elgamal_signature │ └── nonce_reuse.py ├── factorization │ ├── base_conversion.py │ ├── branch_and_prune.py │ ├── complex_multiplication.py │ ├── coppersmith.py │ ├── fermat.py │ ├── gaa.py │ ├── implicit.py │ ├── known_phi.py │ ├── roca.py │ ├── shor.py │ ├── twin_primes.py │ └── unbalanced.py ├── gcm │ └── forbidden_attack.py ├── hnp │ ├── extended_hnp.py │ └── lattice_attack.py ├── ige │ └── padding_oracle.py ├── knapsack │ └── low_density.py ├── lcg │ ├── parameter_recovery.py │ ├── truncated_parameter_recovery.py │ └── truncated_state_recovery.py ├── lwe │ └── arora_ge.py ├── mersenne_twister │ ├── __init__.py │ └── state_recovery.py ├── otp │ └── key_reuse.py ├── pseudoprimes │ └── miller_rabin.py ├── rc4 │ └── fms.py ├── rsa │ ├── bleichenbacher.py │ ├── bleichenbacher_signature_forgery.py │ ├── boneh_durfee.py │ ├── cherkaoui_semmouni.py │ ├── common_modulus.py │ ├── crt_fault_attack.py │ ├── d_fault_attack.py │ ├── desmedt_odlyzko.py │ ├── extended_wiener_attack.py │ ├── hastad_attack.py │ ├── known_crt_exponents.py │ ├── known_d.py │ ├── low_exponent.py │ ├── lsb_oracle.py │ ├── manger.py │ ├── nitaj_crt_rsa.py │ ├── non_coprime_exponent.py │ ├── partial_key_exposure.py │ ├── related_message.py │ ├── stereotyped_message.py │ ├── wiener_attack.py │ ├── wiener_attack_common_prime.py │ └── wiener_attack_lattice.py └── shamir_secret_sharing │ ├── deterministic_coefficients.py │ └── share_forgery.py ├── shared ├── __init__.py ├── complex_multiplication.py ├── crt.py ├── ecc.py ├── hensel.py ├── lattice.py ├── matrices.py ├── partial_integer.py ├── polynomial.py └── small_roots │ ├── __init__.py │ ├── aono.py │ ├── blomer_may.py │ ├── boneh_durfee.py │ ├── coron.py │ ├── coron_direct.py │ ├── ernst.py │ ├── herrmann_may.py │ ├── herrmann_may_multivariate.py │ ├── howgrave_graham.py │ ├── jochemsz_may_integer.py │ ├── jochemsz_may_modular.py │ └── nitaj_fouotsa.py └── test ├── __init__.py ├── shared ├── __init__.py ├── test_ecc.py └── test_shared.py ├── test_acd.py ├── test_cbc.py ├── test_cbc_and_cbc_mac.py ├── test_cbc_mac.py ├── test_ctr.py ├── test_ecb.py ├── test_ecc.py ├── test_elgamal_encryption.py ├── test_elgamal_signature.py ├── test_factorization.py ├── test_gcm.py ├── test_hnp.py ├── test_ige.py ├── test_knapsack.py ├── test_lcg.py ├── test_lwe.py ├── test_mersenne_twister.py ├── test_otp.py ├── test_pseudoprimes.py ├── test_rc4.py ├── test_rsa.py └── test_shamir_secret_sharing.py /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Python tests 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-22.04 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up Python 3.9 20 | uses: actions/setup-python@v3 21 | with: 22 | python-version: "3.9" 23 | - name: Install dependencies 24 | run: | 25 | sudo apt-get install sagemath 26 | sage -pip install pycryptodome 27 | sage -pip install pytest 28 | - name: Test with pytest 29 | run: | 30 | export DOT_SAGE="$HOME/.sage" 31 | PYTHONPATH="$DOT_SAGE/local/lib/python3.10/site-packages" $DOT_SAGE/local/bin/pytest 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .pytest_cache/ 3 | *.py[cod] 4 | 5 | .idea 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Joachim Vandersmissen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /attacks/acd/mp.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from itertools import product 5 | from math import gcd 6 | 7 | from sage.all import ZZ 8 | 9 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 10 | if sys.path[1] != path: 11 | sys.path.insert(1, path) 12 | 13 | from shared import small_roots 14 | 15 | 16 | def attack(N, a, rho, t=1, k=1, roots_method="groebner"): 17 | """ 18 | Solves the ACD problem using the multivariate polynomial approach. 19 | More information: Galbraith D. S. et al., "Algorithms for the Approximate Common Divisor Problem" (Section 5) 20 | :param N: N = p * q0 21 | :param a: the a samples, with ai = p * qi + ri 22 | :param rho: the bit length of the r values 23 | :param t: the parameter t (default: 1) 24 | :param k: the parameter k (default: 1) 25 | :param roots_method: the method to use to find roots (default: "groebner") 26 | :return: the secret integer p and a list containing the r values, or None if p could not be found 27 | """ 28 | assert len(a) > 0, "At least one a value is required." 29 | assert t >= k, "t must be greater than or equal to k." 30 | 31 | R = 2 ** rho 32 | 33 | pr = ZZ[tuple(f"x{i}" for i in range(len(a)))] 34 | x = pr.gens() 35 | X = [R] * len(x) 36 | 37 | logging.debug("Generating shifts...") 38 | 39 | shifts = [] 40 | for i in product(*[range(t + 1) for _ in x]): 41 | if sum(i) <= t: 42 | l = max(k - sum(i), 0) 43 | fi = N ** l 44 | for m in range(len(i)): 45 | fi *= (x[m] - a[m]) ** i[m] 46 | 47 | shifts.append(fi) 48 | 49 | B, monomials = small_roots.create_lattice(pr, shifts, X) 50 | B = small_roots.reduce_lattice(B) 51 | polynomials = small_roots.reconstruct_polynomials(B, None, N ** k, monomials, X) 52 | for roots in small_roots.find_roots(pr, polynomials, method=roots_method): 53 | r = [roots[xi] for xi in x] 54 | if all(-R < ri < R for ri in r): 55 | return int(gcd(N, a[0] - r[0])), r 56 | -------------------------------------------------------------------------------- /attacks/acd/ol.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from sage.all import ZZ 5 | from sage.all import matrix 6 | 7 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 8 | if sys.path[1] != path: 9 | sys.path.insert(1, path) 10 | 11 | from shared import symmetric_mod 12 | 13 | 14 | def attack(x, rho): 15 | """ 16 | Solves the ACD problem using the orthogonal based approach. 17 | More information: Galbraith D. S. et al., "Algorithms for the Approximate Common Divisor Problem" (Section 4) 18 | :param x: the x samples, with xi = p * qi + ri 19 | :param rho: the bit length of the r values 20 | :return: the secret integer p and a list containing the r values, or None if p could not be found 21 | """ 22 | assert len(x) >= 2, "At least two x values are required." 23 | 24 | R = 2 ** rho 25 | 26 | B = matrix(ZZ, len(x), len(x) + 1) 27 | for i, xi in enumerate(x): 28 | B[i, 0] = xi 29 | B[i, i + 1] = R 30 | 31 | B = B.LLL() 32 | 33 | K = B.submatrix(row=0, col=1, nrows=len(x) - 1, ncols=len(x)).right_kernel() 34 | q = K.an_element() 35 | r0 = symmetric_mod(x[0], q[0]) 36 | p = abs((x[0] - r0) // q[0]) 37 | r = [symmetric_mod(xi, p) for xi in x] 38 | if all(-R < ri < R for ri in r): 39 | return int(p), r 40 | -------------------------------------------------------------------------------- /attacks/acd/sda.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from sage.all import ZZ 5 | from sage.all import matrix 6 | 7 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 8 | if sys.path[1] != path: 9 | sys.path.insert(1, path) 10 | 11 | from shared import symmetric_mod 12 | from shared.lattice import shortest_vectors 13 | 14 | 15 | def attack(x, rho): 16 | """ 17 | Solves the ACD problem using the simultaneous Diophantine approximation approach. 18 | More information: Galbraith D. S. et al., "Algorithms for the Approximate Common Divisor Problem" (Section 3) 19 | :param x: the x samples, with xi = p * qi + ri 20 | :param rho: the bit length of the r values 21 | :return: the secret integer p and a list containing the r values, or None if p could not be found 22 | """ 23 | assert len(x) >= 2, "At least two x values are required." 24 | 25 | R = 2 ** (rho + 1) 26 | 27 | B = matrix(ZZ, len(x), len(x)) 28 | B[0, 0] = R 29 | for i in range(1, len(x)): 30 | B[0, i] = x[i] 31 | B[i, i] = -x[0] 32 | 33 | for v in shortest_vectors(B): 34 | if v[0] != 0 and v[0] % R == 0: 35 | q0 = v[0] // R 36 | r0 = symmetric_mod(x[0], q0) 37 | p = abs((x[0] - r0) // q0) 38 | r = [symmetric_mod(xi, p) for xi in x] 39 | if all(-R < ri < R for ri in r): 40 | return int(p), r 41 | -------------------------------------------------------------------------------- /attacks/cbc/bit_flipping.py: -------------------------------------------------------------------------------- 1 | def attack(iv, c, pos, p, p_): 2 | """ 3 | Replaces the original plaintext with a new plaintext at a position in the ciphertext. 4 | :param iv: the initialization vector 5 | :param c: the ciphertext 6 | :param pos: the position to modify at 7 | :param p: the original plaintext 8 | :param p_: the new plaintext 9 | :return: a tuple containing the modified initialization vector and the modified ciphertext 10 | """ 11 | iv_ = bytearray(iv) 12 | c_ = bytearray(c) 13 | for i in range(len(p)): 14 | if pos + i < 16: 15 | iv_[pos + i] = iv[pos + i] ^ p[i] ^ p_[i] 16 | else: 17 | c_[pos + i - 16] = c[pos + i - 16] ^ p[i] ^ p_[i] 18 | 19 | return iv_, c_ 20 | -------------------------------------------------------------------------------- /attacks/cbc/iv_recovery.py: -------------------------------------------------------------------------------- 1 | from Crypto.Util.strxor import strxor 2 | 3 | 4 | def attack(decrypt_oracle): 5 | """ 6 | Recovers the initialization vector using a chosen-ciphertext attack. 7 | :param decrypt_oracle: the decryption oracle to decrypt ciphertexts 8 | :return: the initialization vector 9 | """ 10 | p = decrypt_oracle(bytes(32)) 11 | return strxor(p[:16], p[16:]) 12 | -------------------------------------------------------------------------------- /attacks/cbc/padding_oracle.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from Crypto.Util.strxor import strxor 4 | 5 | 6 | def _attack_block(padding_oracle, iv, c): 7 | logging.info(f"Attacking block {c.hex()}...") 8 | r = bytes() 9 | for i in reversed(range(16)): 10 | s = bytes([16 - i] * (16 - i)) 11 | for b in range(256): 12 | iv_ = bytes(i) + strxor(s, bytes([b]) + r) 13 | if padding_oracle(iv_, c): 14 | r = bytes([b]) + r 15 | break 16 | else: 17 | raise ValueError(f"Unable to find decryption for {s}, {iv}, and {c}") 18 | 19 | return strxor(iv, r) 20 | 21 | 22 | def attack(padding_oracle, iv, c): 23 | """ 24 | Recovers the plaintext using the padding oracle attack. 25 | :param padding_oracle: the padding oracle, returns True if the padding is correct, False otherwise 26 | :param iv: the initialization vector 27 | :param c: the ciphertext 28 | :return: the (padded) plaintext 29 | """ 30 | p = _attack_block(padding_oracle, iv, c[0:16]) 31 | for i in range(16, len(c), 16): 32 | p += _attack_block(padding_oracle, c[i - 16:i], c[i:i + 16]) 33 | 34 | return p 35 | -------------------------------------------------------------------------------- /attacks/cbc_and_cbc_mac/eam_key_reuse.py: -------------------------------------------------------------------------------- 1 | def attack(decrypt_oracle, iv, c, t): 2 | """ 3 | Uses a chosen-ciphertext attack to decrypt the ciphertext. 4 | :param decrypt_oracle: the decryption oracle 5 | :param iv: the initialization vector 6 | :param c: the ciphertext 7 | :param t: the tag corresponding to the ciphertext 8 | :return: the plaintext 9 | """ 10 | c_ = iv + c 11 | p_ = decrypt_oracle(bytes(16), c_, c[-16:]) 12 | return p_[16:] 13 | -------------------------------------------------------------------------------- /attacks/cbc_and_cbc_mac/etm_key_reuse.py: -------------------------------------------------------------------------------- 1 | def attack(encrypt_oracle, decrypt_oracle, iv, c, t): 2 | """ 3 | Uses a chosen-ciphertext attack to decrypt the ciphertext. 4 | :param encrypt_oracle: the encryption oracle 5 | :param decrypt_oracle: the decryption oracle 6 | :param iv: the initialization vector 7 | :param c: the ciphertext 8 | :param t: the tag corresponding to the ciphertext 9 | :return: the plaintext 10 | """ 11 | p_ = bytes(16) + iv + c 12 | iv_, c_, t_ = encrypt_oracle(p_) 13 | c__ = iv + c 14 | p__ = decrypt_oracle(iv_, c__, c_[-32:-16]) 15 | return p__[16:] 16 | -------------------------------------------------------------------------------- /attacks/cbc_and_cbc_mac/mte_key_reuse.py: -------------------------------------------------------------------------------- 1 | def attack(decrypt_oracle, iv, c, encrypted_zeroes): 2 | """ 3 | Uses a chosen-ciphertext attack to decrypt the ciphertext. 4 | Prior knowledge of E_k(0^16) is required for this attack to work. 5 | :param decrypt_oracle: the decryption oracle 6 | :param iv: the initialization vector 7 | :param c: the ciphertext 8 | :param encrypted_zeroes: a full zero block encrypted using the key 9 | :return: the plaintext 10 | """ 11 | c_ = iv + c[:-16] + encrypted_zeroes 12 | p_ = decrypt_oracle(bytes(16), c_) 13 | return p_[16:] 14 | -------------------------------------------------------------------------------- /attacks/cbc_mac/length_extension.py: -------------------------------------------------------------------------------- 1 | from Crypto.Util.strxor import strxor 2 | 3 | 4 | def attack(m1, t1, m2, t2): 5 | """ 6 | Uses a length extension attack to forge a message and tag pair for CBC-MAC. 7 | :param m1: the first message 8 | :param t1: the tag of the first message 9 | :param m2: the second message 10 | :param t2: the tag of the second message 11 | :return: a tuple containing a valid message and tag for CBC-MAC 12 | """ 13 | m3 = bytearray(m1) 14 | m3 += strxor(t1, m2[:16]) 15 | for i in range(16, len(m2), 16): 16 | m3 += m2[i:i + 16] 17 | 18 | return m3, t2 19 | -------------------------------------------------------------------------------- /attacks/ctr/bit_flipping.py: -------------------------------------------------------------------------------- 1 | def attack(c, pos, p, p_): 2 | """ 3 | Replaces the original plaintext with a new plaintext at a position in the ciphertext. 4 | :param c: the ciphertext 5 | :param pos: the position to modify at 6 | :param p: the original plaintext 7 | :param p_: the new plaintext 8 | :return: the modified ciphertext 9 | """ 10 | c_ = bytearray(c) 11 | for i in range(len(p)): 12 | c_[pos + i] = c[pos + i] ^ p[i] ^ p_[i] 13 | 14 | return c_ 15 | -------------------------------------------------------------------------------- /attacks/ctr/crime.py: -------------------------------------------------------------------------------- 1 | def attack(encrypt_oracle, known_prefix, padding_byte): 2 | """ 3 | Recovers a secret using the CRIME attack (CTR version). 4 | :param encrypt_oracle: the encryption oracle 5 | :param known_prefix: a known prefix of the secret to recover 6 | :param padding_byte: a byte which is never used in the plaintext 7 | :return: the secret 8 | """ 9 | known_prefix = bytearray(known_prefix) 10 | padding_bytes = bytes([padding_byte]) 11 | while True: 12 | for i in range(256): 13 | # Don't try the padding byte. 14 | if i == padding_byte: 15 | continue 16 | 17 | l1 = len(encrypt_oracle(padding_bytes + known_prefix + bytes([i]) + padding_bytes + padding_bytes)) 18 | l2 = len(encrypt_oracle(padding_bytes + known_prefix + padding_bytes + bytes([i]) + padding_bytes)) 19 | if l1 < l2: 20 | known_prefix.append(i) 21 | break 22 | else: 23 | return known_prefix 24 | -------------------------------------------------------------------------------- /attacks/ctr/separator_oracle.py: -------------------------------------------------------------------------------- 1 | def _find_separator_positions(separator_oracle, c): 2 | separator_positions = [] 3 | c = bytearray(c) 4 | for i in range(len(c)): 5 | c[i] ^= 1 6 | valid = separator_oracle(c) 7 | c[i] ^= 1 8 | if not valid: 9 | c[i] ^= 2 10 | valid = separator_oracle(c) 11 | c[i] ^= 2 12 | if not valid: 13 | separator_positions.append(i) 14 | 15 | return separator_positions 16 | 17 | 18 | def attack(separator_oracle, separator_byte, c): 19 | """ 20 | Recovers the plaintext using the separator oracle attack. 21 | :param separator_oracle: the separator oracle, returns True if the separators are correct, False otherwise 22 | :param separator_byte: the separator which is used in the separator oracle 23 | :param c: the ciphertext 24 | :return: the plaintext 25 | """ 26 | separator_positions = _find_separator_positions(separator_oracle, c) 27 | c = bytearray(c) 28 | # Ensure that at least 1 separator is missing. 29 | c[separator_positions[0]] ^= 1 30 | p = bytearray(len(c)) 31 | for i in range(len(c)): 32 | if i in separator_positions: 33 | p[i] = separator_byte 34 | else: 35 | c_i = c[i] 36 | # Try every byte until an additional separator is created. 37 | for b in range(256): 38 | c[i] = b 39 | if separator_oracle(c): 40 | p[i] = c_i ^ c[i] ^ separator_byte 41 | break 42 | 43 | c[i] = c_i 44 | 45 | return p 46 | -------------------------------------------------------------------------------- /attacks/ecb/plaintext_recovery.py: -------------------------------------------------------------------------------- 1 | def attack(encrypt_oracle, unused_byte=0): 2 | """ 3 | Recovers a secret which is appended to a plaintext and encrypted using ECB. 4 | :param encrypt_oracle: the encryption oracle 5 | :param unused_byte: a byte that's never used in the secret 6 | :return: the secret 7 | """ 8 | paddings = [bytes([unused_byte] * i) for i in range(16)] 9 | secret = bytearray() 10 | while True: 11 | padding = paddings[15 - (len(secret) % 16)] 12 | p = bytearray(padding + secret + b"0" + padding) 13 | byte_index = len(padding) + len(secret) 14 | end1 = len(padding) + len(secret) + 1 15 | end2 = end1 + len(padding) + len(secret) + 1 16 | for i in range(256): 17 | p[byte_index] = i 18 | c = encrypt_oracle(p) 19 | if c[end1 - 16:end1] == c[end2 - 16:end2]: 20 | secret.append(i) 21 | break 22 | else: 23 | secret.pop() 24 | break 25 | 26 | return bytes(secret) 27 | -------------------------------------------------------------------------------- /attacks/ecb/plaintext_recovery_harder.py: -------------------------------------------------------------------------------- 1 | def _get_prefix_padding(encrypt_oracle, paddings): 2 | check = b"\x01" * 32 3 | for i in range(16): 4 | prefix_padding = paddings[16 - i] 5 | c = encrypt_oracle(prefix_padding + check) 6 | if c[16:32] == c[32:48]: 7 | return prefix_padding 8 | 9 | 10 | def attack(encrypt_oracle, unused_byte=0): 11 | """ 12 | Recovers a secret which is appended to a plaintext and encrypted using ECB. 13 | In this scenario, the encryption oracle prepends a constant, random prefix (length 0 to 16) to the plaintext. 14 | :param encrypt_oracle: the encryption oracle 15 | :param unused_byte: a byte that's never used in the secret or random prefix 16 | :return: the secret 17 | """ 18 | # 17 here because _get_prefix_padding needs paddings[16]. 19 | paddings = [bytes([unused_byte] * i) for i in range(17)] 20 | prefix_padding = _get_prefix_padding(encrypt_oracle, paddings) 21 | secret = bytearray() 22 | while True: 23 | padding = paddings[15 - (len(secret) % 16)] 24 | p = bytearray(prefix_padding + padding + secret + b"0" + padding) 25 | byte_index = len(prefix_padding) + len(padding) + len(secret) 26 | end1 = 16 + len(padding) + len(secret) + 1 27 | end2 = end1 + len(padding) + len(secret) + 1 28 | for i in range(256): 29 | p[byte_index] = i 30 | c = encrypt_oracle(p) 31 | if c[end1 - 16:end1] == c[end2 - 16:end2]: 32 | secret.append(i) 33 | break 34 | else: 35 | secret.pop() 36 | break 37 | 38 | return bytes(secret) 39 | -------------------------------------------------------------------------------- /attacks/ecb/plaintext_recovery_hardest.py: -------------------------------------------------------------------------------- 1 | def attack(encrypt_oracle, unused_byte=0): 2 | """ 3 | Recovers a secret which is appended to a plaintext and encrypted using ECB. 4 | In this scenario, the encryption oracle prepends a random prefix (length 0 to 16) to the plaintext. 5 | :param encrypt_oracle: the encryption oracle 6 | :param unused_byte: a byte that's never used in the secret or random prefix 7 | :return: the secret 8 | """ 9 | paddings = [bytes([unused_byte] * i) for i in range(16)] 10 | prefix = bytes([unused_byte] * 32) 11 | secret = bytearray() 12 | while True: 13 | padding = paddings[15 - (len(secret) % 16)] 14 | p = bytearray(prefix + padding + secret + b"0" + padding) 15 | byte_index = len(prefix) + len(padding) + len(secret) 16 | end1 = len(prefix) + len(padding) + len(secret) + 1 17 | end2 = end1 + len(padding) + len(secret) + 1 18 | for i in range(256): 19 | p[byte_index] = i 20 | c = encrypt_oracle(p) 21 | while c[0:16] != c[16:32]: 22 | c = encrypt_oracle(p) 23 | 24 | if c[end1 - 16:end1] == c[end2 - 16:end2]: 25 | secret.append(i) 26 | break 27 | else: 28 | secret.pop() 29 | break 30 | 31 | return bytes(secret) 32 | -------------------------------------------------------------------------------- /attacks/ecc/ecdsa_nonce_reuse.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 5 | if sys.path[1] != path: 6 | sys.path.insert(1, path) 7 | 8 | from shared import solve_congruence 9 | 10 | 11 | def attack(n, m1, r1, s1, m2, r2, s2): 12 | """ 13 | Recovers the nonce and private key from two messages signed using the same nonce. 14 | :param n: the order of the elliptic curve 15 | :param m1: the first message 16 | :param r1: the signature of the first message 17 | :param s1: the signature of the first message 18 | :param m2: the second message 19 | :param r2: the signature of the second message 20 | :param s2: the signature of the second message 21 | :return: generates tuples containing the possible nonce and private key 22 | """ 23 | for k in solve_congruence(int(s1 - s2), int(m1 - m2), int(n)): 24 | for x in solve_congruence(int(r1), int(k * s1 - m1), int(n)): 25 | yield int(k), int(x) 26 | -------------------------------------------------------------------------------- /attacks/ecc/frey_ruck_attack.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from math import gcd 5 | 6 | from sage.all import GF 7 | 8 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 9 | if sys.path[1] != path: 10 | sys.path.insert(1, path) 11 | 12 | from shared.ecc import get_embedding_degree 13 | 14 | 15 | def attack(P, R, max_k=6, max_tries=10): 16 | """ 17 | Solves the discrete logarithm problem using the Frey-Ruck attack. 18 | More information: Harasawa R. et al., "Comparing the MOV and FR Reductions in Elliptic Curve Cryptography" (Section 3) 19 | :param P: the base point 20 | :param R: the point multiplication result 21 | :param max_k: the maximum value of embedding degree to try (default: 6) 22 | :param max_tries: the maximum amount of times to try to find l (default: 10) 23 | :return: l such that l * P == R, or None if l was not found 24 | """ 25 | E = P.curve() 26 | q = E.base_ring().order() 27 | n = P.order() 28 | assert gcd(n, q) == 1, "GCD of base point order and curve base ring order should be 1." 29 | 30 | logging.info("Calculating embedding degree...") 31 | k = get_embedding_degree(q, n, max_k) 32 | if k is None: 33 | return None 34 | 35 | logging.info(f"Found embedding degree {k}") 36 | Ek = E.base_extend(GF(q ** k)) 37 | Pk = Ek(P) 38 | Rk = Ek(R) 39 | for _ in range(max_tries): 40 | S = Ek.random_point() 41 | T = Ek.random_point() 42 | if (gamma := Pk.tate_pairing(S, n, k) / Pk.tate_pairing(T, n, k)) == 1: 43 | continue 44 | 45 | delta = Rk.tate_pairing(S, n, k) / Rk.tate_pairing(T, n, k) 46 | logging.info(f"Computing {delta}.log({gamma})...") 47 | l = delta.log(gamma) 48 | return int(l) 49 | 50 | return None 51 | -------------------------------------------------------------------------------- /attacks/ecc/mov_attack.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from math import gcd 5 | 6 | from sage.all import GF 7 | 8 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 9 | if sys.path[1] != path: 10 | sys.path.insert(1, path) 11 | 12 | from shared.ecc import get_embedding_degree 13 | 14 | 15 | def attack(P, R, max_k=6, max_tries=10): 16 | """ 17 | Solves the discrete logarithm problem using the MOV attack. 18 | More information: Harasawa R. et al., "Comparing the MOV and FR Reductions in Elliptic Curve Cryptography" (Section 2) 19 | :param P: the base point 20 | :param R: the point multiplication result 21 | :param max_k: the maximum value of embedding degree to try (default: 6) 22 | :param max_tries: the maximum amount of times to try to find l (default: 10) 23 | :return: l such that l * P == R, or None if l was not found 24 | """ 25 | E = P.curve() 26 | q = E.base_ring().order() 27 | n = P.order() 28 | assert gcd(n, q) == 1, "GCD of base point order and curve base ring order should be 1." 29 | 30 | logging.info("Calculating embedding degree...") 31 | k = get_embedding_degree(q, n, max_k) 32 | if k is None: 33 | return None 34 | 35 | logging.info(f"Found embedding degree {k}") 36 | Ek = E.base_extend(GF(q ** k)) 37 | Pk = Ek(P) 38 | Rk = Ek(R) 39 | for i in range(max_tries): 40 | Q_ = Ek.random_point() 41 | m = Q_.order() 42 | d = gcd(m, n) 43 | Q = (m // d) * Q_ 44 | if Q.order() != n: 45 | continue 46 | 47 | if (alpha := Pk.weil_pairing(Q, n)) == 1: 48 | continue 49 | 50 | beta = Rk.weil_pairing(Q, n) 51 | logging.info(f"Computing {beta}.log({alpha})...") 52 | l = beta.log(alpha) 53 | return int(l) 54 | 55 | return None 56 | -------------------------------------------------------------------------------- /attacks/ecc/parameter_recovery.py: -------------------------------------------------------------------------------- 1 | def attack(p, x1, y1, x2, y2): 2 | """ 3 | Recovers the a and b parameters from an elliptic curve when two points are known. 4 | :param p: the prime of the curve base ring 5 | :param x1: the x coordinate of the first point 6 | :param y1: the y coordinate of the first point 7 | :param x2: the x coordinate of the second point 8 | :param y2: the y coordinate of the second point 9 | :return: a tuple containing the a and b parameters of the elliptic curve 10 | """ 11 | a = pow(x1 - x2, -1, p) * (pow(y1, 2, p) - pow(y2, 2, p) - (pow(x1, 3, p) - pow(x2, 3, p))) % p 12 | b = (pow(y1, 2, p) - pow(x1, 3, p) - a * x1) % p 13 | return int(a), int(b) 14 | -------------------------------------------------------------------------------- /attacks/ecc/singular_curve.py: -------------------------------------------------------------------------------- 1 | from sage.all import GF 2 | 3 | 4 | def attack(p, a2, a4, a6, Gx, Gy, Px, Py): 5 | """ 6 | Solves the discrete logarithm problem on a singular curve (y^2 = x^3 + a2 * x^2 + a4 * x + a6). 7 | :param p: the prime of the curve base ring 8 | :param a2: the a2 parameter of the curve 9 | :param a4: the a4 parameter of the curve 10 | :param a6: the a6 parameter of the curve 11 | :param Gx: the base point x value 12 | :param Gy: the base point y value 13 | :param Px: the point multiplication result x value 14 | :param Py: the point multiplication result y value 15 | :return: l such that l * G == P 16 | """ 17 | x = GF(p)["x"].gen() 18 | f = x ** 3 + a2 * x ** 2 + a4 * x + a6 19 | roots = f.roots() 20 | 21 | # Singular point is a cusp. 22 | if len(roots) == 1: 23 | alpha = roots[0][0] 24 | u = (Gx - alpha) / Gy 25 | v = (Px - alpha) / Py 26 | return int(v / u) 27 | 28 | # Singular point is a node. 29 | if len(roots) == 2: 30 | if roots[0][1] == 2: 31 | alpha = roots[0][0] 32 | beta = roots[1][0] 33 | elif roots[1][1] == 2: 34 | alpha = roots[1][0] 35 | beta = roots[0][0] 36 | else: 37 | raise ValueError("Expected root with multiplicity 2.") 38 | 39 | t = (alpha - beta).sqrt() 40 | u = (Gy + t * (Gx - alpha)) / (Gy - t * (Gx - alpha)) 41 | v = (Py + t * (Px - alpha)) / (Py - t * (Px - alpha)) 42 | return int(v.log(u)) 43 | 44 | raise ValueError(f"Unexpected number of roots {len(roots)}.") 45 | -------------------------------------------------------------------------------- /attacks/ecc/smart_attack.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from sage.all import EllipticCurve 3 | from sage.all import Qq 4 | from sage.all import ZZ 5 | 6 | 7 | # Convert a field element to a p-adic number. 8 | def _gf_to_qq(n, qq, x): 9 | return ZZ(x) if n == 1 else qq(list(map(int, x.polynomial()))) 10 | 11 | 12 | # Lift a point to the p-adic numbers. 13 | def _lift(E, p, Px, Py): 14 | for P in E.lift_x(Px, all=True): 15 | if (P.xy()[1] % p) == Py: 16 | return P 17 | 18 | 19 | def attack(G, P): 20 | """ 21 | Solves the discrete logarithm problem using Smart's attack. 22 | More information: Smart N. P., "The Discrete Logarithm Problem on Elliptic Curves of Trace One" 23 | More information: Hofman S. J., "The Discrete Logarithm Problem on Anomalous Elliptic Curves" (Section 6) 24 | :param G: the base point 25 | :param P: the point multiplication result 26 | :return: l such that l * G == P 27 | """ 28 | E = G.curve() 29 | assert E.trace_of_frobenius() == 1, f"Curve should have trace of Frobenius = 1." 30 | 31 | F = E.base_ring() 32 | p = F.characteristic() 33 | q = F.order() 34 | n = F.degree() 35 | qq = Qq(q, names="g") 36 | 37 | # Section 6.1: case where n == 1 38 | logging.info(f"Computing l % {p}...") 39 | E = EllipticCurve(qq, [_gf_to_qq(n, qq, a) + q * ZZ.random_element(1, q) for a in E.a_invariants()]) 40 | Gx, Gy = _gf_to_qq(n, qq, G.xy()[0]), _gf_to_qq(n, qq, G.xy()[1]) 41 | Gx, Gy = (q * _lift(E, p, Gx, Gy)).xy() 42 | Px, Py = _gf_to_qq(n, qq, P.xy()[0]), _gf_to_qq(n, qq, P.xy()[1]) 43 | Px, Py = (q * _lift(E, p, Px, Py)).xy() 44 | l = ZZ(((Px / Py) / (Gx / Gy)) % p) 45 | 46 | if n > 1: 47 | # Section 6.2: case where n > 1 48 | G0 = p ** (n - 1) * G 49 | G0x, G0y = _gf_to_qq(n, qq, G0.xy()[0]), _gf_to_qq(n, qq, G0.xy()[1]) 50 | G0x, G0y = (q * _lift(E, p, G0x, G0y)).xy() 51 | for i in range(1, n): 52 | logging.info(f"Computing l % {p ** (i + 1)}...") 53 | Pi = p ** (n - i - 1) * (P - l * G) 54 | if Pi.is_zero(): 55 | continue 56 | 57 | Pix, Piy = _gf_to_qq(n, qq, Pi.xy()[0]), _gf_to_qq(n, qq, Pi.xy()[1]) 58 | Pix, Piy = (q * _lift(E, p, Pix, Piy)).xy() 59 | l += p ** i * ZZ(((Pix / Piy) / (G0x / G0y)) % p) 60 | 61 | return int(l) 62 | -------------------------------------------------------------------------------- /attacks/elgamal_encryption/nonce_reuse.py: -------------------------------------------------------------------------------- 1 | def attack(p, m, c1, c2, c1_, c2_): 2 | """ 3 | Recovers a secret plaintext encrypted using the same nonce as a previous, known plaintext. 4 | :param p: the prime used in the ElGamal scheme 5 | :param m: the known plaintext 6 | :param c1: the ciphertext of the known plaintext 7 | :param c2: the ciphertext of the known plaintext 8 | :param c1_: the ciphertext of the secret plaintext 9 | :param c2_: the ciphertext of the secret plaintext 10 | :return: the secret plaintext 11 | """ 12 | s = c2 * pow(m, -1, p) % p 13 | m_ = c2_ * pow(s, -1, p) % p 14 | return int(m_) 15 | -------------------------------------------------------------------------------- /attacks/elgamal_encryption/unsafe_generator.py: -------------------------------------------------------------------------------- 1 | from sage.all import legendre_symbol 2 | 3 | 4 | def attack(p, h, c1, c2): 5 | """ 6 | Returns the Legendre symbol of the message encrypted using an unsafe generator. 7 | :param p: the prime used in the ElGamal scheme 8 | :param h: the public key 9 | :param c1: the ciphertext 10 | :param c2: the ciphertext 11 | :return: the Legendre symbol 12 | """ 13 | return int(legendre_symbol(c2, p) // max(legendre_symbol(h, p), legendre_symbol(c1, p))) 14 | -------------------------------------------------------------------------------- /attacks/elgamal_signature/nonce_reuse.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 5 | if sys.path[1] != path: 6 | sys.path.insert(1, path) 7 | 8 | from shared import solve_congruence 9 | 10 | 11 | def attack(p, m1, r1, s1, m2, r2, s2): 12 | """ 13 | Recovers the nonce and private key from two messages signed using the same nonce. 14 | :param p: the prime used in the ElGamal scheme 15 | :param m1: the first message 16 | :param r1: the signature of the first message 17 | :param s1: the signature of the first message 18 | :param m2: the second message 19 | :param r2: the signature of the second message 20 | :param s2: the signature of the second message 21 | :return: generates tuples containing the possible nonce and private key 22 | """ 23 | for k in solve_congruence(s1 - s2, m1 - m2, p - 1): 24 | for x in solve_congruence(r1, m1 - k * s1, p - 1): 25 | yield int(k), int(x) 26 | -------------------------------------------------------------------------------- /attacks/factorization/base_conversion.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sage.all import ZZ 4 | 5 | 6 | def factorize(N, coefficient_threshold=32): 7 | """ 8 | Recovers the prime factors from a modulus by converting it to different bases. 9 | :param N: the modulus 10 | :param coefficient_threshold: the threshold of coefficients below which we will try to factor a base k polynomial 11 | :return: a tuple containing the prime factors 12 | """ 13 | R = ZZ["x"] 14 | base = 2 15 | while True: 16 | logging.debug(f"Trying {base = }...") 17 | poly = R(ZZ(N).digits(base)) 18 | logging.debug(f"Got {len(poly.coefficients())} coefficients") 19 | if len(poly.coefficients()) < coefficient_threshold: 20 | facs = poly.factor() 21 | return tuple(map(lambda f: int(f[0](base)), facs)) 22 | 23 | base += 1 24 | 25 | 26 | def factorize_base_2x(N): 27 | """ 28 | Recovers the prime factors from a modulus by converting it to different bases of the form 2^x. 29 | :param N: the modulus 30 | :return: a tuple containing the prime factors 31 | """ 32 | R = ZZ["x"] 33 | base = 2 34 | while True: 35 | logging.debug(f"Trying {base = }...") 36 | poly = R(ZZ(N).digits(base)) 37 | facs = poly.factor() 38 | if len(facs) > 1: 39 | return tuple(map(lambda f: int(f[0](base)), facs)) 40 | 41 | base *= 2 42 | -------------------------------------------------------------------------------- /attacks/factorization/complex_multiplication.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from math import gcd 5 | 6 | from sage.all import EllipticCurve 7 | from sage.all import Zmod 8 | from sage.all import hilbert_class_polynomial 9 | 10 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 11 | if sys.path[1] != path: 12 | sys.path.insert(1, path) 13 | 14 | from shared.polynomial import polynomial_inverse 15 | from shared.polynomial import polynomial_xgcd 16 | 17 | 18 | def factorize(N, D): 19 | """ 20 | Recovers the prime factors from a modulus using Cheng's elliptic curve complex multiplication method. 21 | More information: Sedlacek V. et al., "I want to break square-free: The 4p - 1 factorization method and its RSA backdoor viability" 22 | :param N: the modulus 23 | :param D: the discriminant to use to generate the Hilbert polynomial 24 | :return: a tuple containing the prime factors 25 | """ 26 | assert D % 8 == 3, "D should be square-free" 27 | 28 | zmodn = Zmod(N) 29 | pr = zmodn["x"] 30 | 31 | H = pr(hilbert_class_polynomial(-D)) 32 | Q = pr.quotient(H) 33 | j = Q.gen() 34 | 35 | try: 36 | k = j * polynomial_inverse((1728 - j).lift(), H) 37 | except ArithmeticError as err: 38 | # If some polynomial was not invertible during XGCD calculation, we can factor n. 39 | p = gcd(int(err.args[1].lc()), N) 40 | return int(p), int(N // p) 41 | 42 | E = EllipticCurve(Q, [3 * k, 2 * k]) 43 | while True: 44 | x = zmodn.random_element() 45 | 46 | logging.debug(f"Calculating division polynomial of Q{x}...") 47 | z = E.division_polynomial(N, x=Q(x)) 48 | 49 | try: 50 | d, _, _ = polynomial_xgcd(z.lift(), H) 51 | except ArithmeticError as err: 52 | # If some polynomial was not invertible during XGCD calculation, we can factor n. 53 | p = gcd(int(err.args[1].lc()), N) 54 | return int(p), int(N // p) 55 | 56 | p = gcd(int(d), N) 57 | if 1 < p < N: 58 | return int(p), int(N // p) 59 | -------------------------------------------------------------------------------- /attacks/factorization/coppersmith.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from math import ceil 5 | from math import log 6 | from math import pi 7 | from math import sqrt 8 | 9 | from sage.all import ZZ 10 | from sage.all import Zmod 11 | 12 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 13 | if sys.path[1] != path: 14 | sys.path.insert(1, path) 15 | 16 | from shared.small_roots import coron_direct 17 | from shared.small_roots import herrmann_may_multivariate 18 | from shared.small_roots import howgrave_graham 19 | 20 | 21 | def factorize_p(N, partial_p, beta=0.5, epsilon=0.125, m=None, t=None): 22 | """ 23 | Recover the prime factors from a modulus using Coppersmith's method and bits of one prime factor p are known. 24 | More information: May A., "New RSA Vulnerabilities Using Lattice Reduction Methods" (Section 3.2) 25 | More information: Herrmann M., May A., "Solving Linear Equations Modulo Divisors: On Factoring Given Any Bits" (Section 3 and 4) 26 | :param N: the modulus 27 | :param partial_p: the partial prime factor p (PartialInteger) 28 | :param beta: the parameter beta (default: 0.5) 29 | :param epsilon: the parameter epsilon (default: 0.125) 30 | :param m: the number of normal shifts to use (default: automatically computed using beta and epsilon) 31 | :param t: the number of additional shifts to use (default: automatically computed using beta and epsilon) 32 | :return: a tuple containing the prime factors, or None if the factors could not be found 33 | """ 34 | n = partial_p.unknowns 35 | assert n > 0 36 | if n == 1: 37 | m = ceil(max(beta ** 2 / epsilon, 7 * beta)) if m is None else m 38 | t = int((1 / beta - 1) * m) if t is None else t 39 | small_roots = howgrave_graham.modular_univariate 40 | elif n == 2: 41 | m = ceil((3 * beta * (1 + sqrt(1 - beta))) / epsilon) if m is None else m 42 | t = int((1 - sqrt(1 - beta)) * m) if t is None else t 43 | small_roots = herrmann_may_multivariate.modular_multivariate 44 | else: 45 | m = ceil((n * (1 / pi * (1 - beta) ** (-0.278465) - beta * log(1 - beta))) / epsilon) if m is None else m 46 | t = int((1 - (1 - beta) ** (1 / n)) * m) if t is None else t 47 | small_roots = herrmann_may_multivariate.modular_multivariate 48 | 49 | x = Zmod(N)[tuple(f"x{i}" for i in range(n))].gens() 50 | f = partial_p.sub(x) 51 | X = partial_p.get_unknown_bounds() 52 | logging.info(f"Trying {m = }, {t = }...") 53 | for roots in small_roots(f, N, m, t, X): 54 | p = partial_p.sub(roots) 55 | if 1 < p < N and N % p == 0: 56 | return p, N // p 57 | 58 | return None 59 | 60 | 61 | def factorize_pq(N, partial_p, partial_q, k=None): 62 | """ 63 | Recover the prime factors from a modulus using Coppersmith's method and bits of both prime factors p and q are known. 64 | :param N: the modulus 65 | :param partial_p: the partial prime factor p (PartialInteger) 66 | :param partial_q: the partial prime factor q (PartialInteger) 67 | :param k: the number of shifts to use for Coron's method, must be set if the total number of unknown components is two (default: None) 68 | :return: a tuple containing the prime factors, or None if the factors could not be found 69 | """ 70 | np = partial_p.unknowns 71 | nq = partial_q.unknowns 72 | assert np > 0 and nq > 0 73 | 74 | x = ZZ[tuple(f"x{i}" for i in range(np + nq))].gens() 75 | f = partial_p.sub(x[:np]) * partial_q.sub(x[np:]) - N 76 | Xp = partial_p.get_unknown_bounds() 77 | Xq = partial_q.get_unknown_bounds() 78 | 79 | if np == 1 and nq == 1: 80 | assert k is not None, "k must be set if the total number of unknown components is two." 81 | logging.info(f"Trying {k = }...") 82 | for x0, x1 in coron_direct.integer_bivariate(f, k, Xp[0], Xq[0]): 83 | p = partial_p.sub([x0]) 84 | q = partial_q.sub([x1]) 85 | if p * q == N: 86 | return p, q 87 | else: 88 | # TODO: Jochemsz-May multivariate integer roots? 89 | # Or "Factoring RSA Modulus with Known Bits from Both p and q: A Lattice Method"? 90 | pass 91 | 92 | return None 93 | -------------------------------------------------------------------------------- /attacks/factorization/fermat.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from math import isqrt 5 | 6 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 7 | if sys.path[1] != path: 8 | sys.path.insert(1, path) 9 | 10 | from shared import is_square 11 | 12 | 13 | def factorize(N): 14 | """ 15 | Recovers the prime factors from a modulus using Fermat's factorization method. 16 | :param N: the modulus 17 | :return: a tuple containing the prime factors, or None if the factors were not found 18 | """ 19 | a = isqrt(N) 20 | b = a * a - N 21 | while b < 0 or not is_square(b): 22 | a += 1 23 | b = a * a - N 24 | 25 | p = a - isqrt(b) 26 | q = N // p 27 | return p, q if p * q == N else None 28 | -------------------------------------------------------------------------------- /attacks/factorization/gaa.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | 3 | from sage.all import ZZ 4 | from sage.all import sqrt 5 | 6 | 7 | def factorize(N, rp, rq): 8 | """ 9 | Recovers the prime factors from a modulus using the Ghafar-Ariffin-Asbullah attack. 10 | More information: Ghafar AHA. et al., "A New LSB Attack on Special-Structured RSA Primes" 11 | :param N: the modulus 12 | :param rp: the value rp 13 | :param rq: the value rq 14 | :return: a tuple containing the prime factors 15 | """ 16 | i = ceil(sqrt(rp * rq)) 17 | x = ZZ["x"].gen() 18 | while True: 19 | sigma = (round(int(sqrt(N))) - i) ** 2 20 | z = (N - (rp * rq)) % sigma 21 | f = x ** 2 - z * x + sigma * rp * rq 22 | for x0 in f.roots(multiplicities=False): 23 | if x0 % rp == 0: 24 | p = int((x0 // rp) + rq) 25 | assert N % p == 0 26 | return p, N // p 27 | if x0 % rq == 0: 28 | p = int((x0 // rq) + rp) 29 | assert N % p == 0 30 | return p, N // p 31 | 32 | i += 1 33 | -------------------------------------------------------------------------------- /attacks/factorization/implicit.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from math import gcd 4 | 5 | from sage.all import ZZ 6 | from sage.all import matrix 7 | 8 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 9 | if sys.path[1] != path: 10 | sys.path.insert(1, path) 11 | 12 | from shared.lattice import shortest_vectors 13 | 14 | 15 | def _recover_factors(L, N): 16 | for v in shortest_vectors(L): 17 | factors = [] 18 | for i, Ni in enumerate(N): 19 | qi = gcd(v[i], Ni) 20 | if 1 < qi < Ni and Ni % qi == 0: 21 | factors.append((Ni // qi, qi)) 22 | 23 | if len(factors) == len(N): 24 | return factors 25 | 26 | 27 | def factorize_msb(N, n, t): 28 | """ 29 | Factorizes the moduli when some most significant bits are equal among multiples of a prime factor. 30 | More information: Nitaj A., Ariffin MRK., "Implicit factorization of unbalanced RSA moduli" (Section 4) 31 | :param N: the moduli 32 | :param n: the bit length of the moduli 33 | :param t: the number of shared most significant bits 34 | :return: a list containing a tuple of the factors of each modulus, or None if the factors were not found 35 | """ 36 | L = matrix(ZZ, len(N), len(N)) 37 | L[0, 0] = 2 ** (n - t) 38 | for i in range(1, len(N)): 39 | L[0, i] = N[i] 40 | 41 | for i in range(1, len(N)): 42 | L[i, i] = -N[0] 43 | 44 | return _recover_factors(L, N) 45 | 46 | 47 | def factorize_lsb(N, n, t): 48 | """ 49 | Factorizes the moduli when some least significant bits are equal among multiples of a prime factor. 50 | More information: Nitaj A., Ariffin MRK., "Implicit factorization of unbalanced RSA moduli" (Section 6) 51 | :param N: the moduli 52 | :param n: the bit length of the moduli 53 | :param t: the number of shared least significant bits 54 | :return: a list containing a tuple of the factors of each modulus, or None if the factors were not found 55 | """ 56 | L = matrix(ZZ, len(N), len(N)) 57 | L[0, 0] = 1 58 | for i in range(1, len(N)): 59 | L[0, i] = N[i] * pow(N[0], -1, 2 ** t) % (2 ** t) 60 | 61 | for i in range(1, len(N)): 62 | L[i, i] = -2 ** t 63 | 64 | return _recover_factors(L, N) 65 | -------------------------------------------------------------------------------- /attacks/factorization/known_phi.py: -------------------------------------------------------------------------------- 1 | from math import gcd 2 | from math import isqrt 3 | from random import randrange 4 | 5 | from sage.all import is_prime 6 | 7 | 8 | def factorize(N, phi): 9 | """ 10 | Recovers the prime factors from a modulus if Euler's totient is known. 11 | This method only works for a modulus consisting of 2 primes! 12 | :param N: the modulus 13 | :param phi: Euler's totient, the order of the multiplicative group modulo N 14 | :return: a tuple containing the prime factors, or None if the factors were not found 15 | """ 16 | s = N + 1 - phi 17 | d = s ** 2 - 4 * N 18 | p = int(s - isqrt(d)) // 2 19 | q = int(s + isqrt(d)) // 2 20 | return p, q if p * q == N else None 21 | 22 | 23 | def factorize_multi_prime(N, phi): 24 | """ 25 | Recovers the prime factors from a modulus if Euler's totient is known. 26 | This method works for a modulus consisting of any number of primes, but is considerably be slower than factorize. 27 | More information: Hinek M. J., Low M. K., Teske E., "On Some Attacks on Multi-prime RSA" (Section 3) 28 | :param N: the modulus 29 | :param phi: Euler's totient, the order of the multiplicative group modulo N 30 | :return: a tuple containing the prime factors 31 | """ 32 | prime_factors = set() 33 | factors = [N] 34 | while len(factors) > 0: 35 | # Element to factorize. 36 | N = factors[0] 37 | 38 | w = randrange(2, N - 1) 39 | i = 1 40 | while phi % (2 ** i) == 0: 41 | sqrt_1 = pow(w, phi // (2 ** i), N) 42 | if sqrt_1 > 1 and sqrt_1 != N - 1: 43 | # We can remove the element to factorize now, because we have a factorization. 44 | factors = factors[1:] 45 | 46 | p = gcd(N, sqrt_1 + 1) 47 | q = N // p 48 | 49 | if is_prime(p): 50 | prime_factors.add(p) 51 | elif p > 1: 52 | factors.append(p) 53 | 54 | if is_prime(q): 55 | prime_factors.add(q) 56 | elif q > 1: 57 | factors.append(q) 58 | 59 | # Continue in the outer loop 60 | break 61 | 62 | i += 1 63 | 64 | return tuple(prime_factors) 65 | -------------------------------------------------------------------------------- /attacks/factorization/roca.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from math import log2 5 | 6 | from sage.all import Zmod 7 | from sage.all import factor 8 | 9 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 10 | if sys.path[1] != path: 11 | sys.path.insert(1, path) 12 | 13 | from shared.small_roots import howgrave_graham 14 | 15 | 16 | def _prime_power_divisors(M): 17 | divisors = [] 18 | for p, e in factor(M): 19 | for i in range(1, e + 1): 20 | divisors.append(p ** i) 21 | 22 | divisors.sort() 23 | return divisors 24 | 25 | 26 | # Algorithm 2. 27 | def compute_max_M_(M, ord_): 28 | for p in _prime_power_divisors(M): 29 | ordp = Zmod(p)(65537).multiplicative_order() 30 | if ord_ % ordp != 0: 31 | M //= p 32 | 33 | return M 34 | 35 | 36 | # Section 2.7.2. 37 | def _greedy_find_M_(n, M): 38 | ord = Zmod(M)(65537).multiplicative_order() 39 | while True: 40 | best_r = 0 41 | best_ord_ = ord 42 | best_M_ = M 43 | for p in _prime_power_divisors(ord): 44 | ord_ = ord // p 45 | M_ = compute_max_M_(M, ord_) 46 | r = (log2(ord) - log2(ord_)) / (log2(M) - log2(M_)) 47 | if r > best_r: 48 | best_r = r 49 | best_ord_ = ord_ 50 | best_M_ = M_ 51 | 52 | if log2(best_M_) < log2(n) / 4: 53 | return M 54 | 55 | ord = best_ord_ 56 | M = best_M_ 57 | 58 | 59 | def factorize(N, M, m, t, g=65537): 60 | """ 61 | Recovers the prime factors from a modulus using the ROCA method. 62 | More information: Nemec M. et al., "The Return of Coppersmith’s Attack: Practical Factorization of Widely Used RSA Moduli" 63 | :param N: the modulus 64 | :param M: the primorial used to generate the primes 65 | :param m: the m parameter for Coppersmith's method 66 | :param t: the t parameter for Coppersmith's method 67 | :param g: the generator value (default: 65537) 68 | :return: a tuple containing the prime factors, or None if the factors were not found 69 | """ 70 | logging.info("Generating M'...") 71 | M_ = _greedy_find_M_(N, M) 72 | zmodm_ = Zmod(M_) 73 | g = zmodm_(g) 74 | c_ = zmodm_(N).log(g) 75 | ord_ = g.multiplicative_order() 76 | 77 | x = Zmod(N)["x"].gen() 78 | X = int(2 * N ** 0.5 // M_) 79 | logging.info("Starting exhaustive a' search...") 80 | for a_ in range(c_ // 2, (c_ + ord_) // 2 + 1): 81 | f = M_ * x + int(g ** a_) 82 | for k_, in howgrave_graham.modular_univariate(f, N, m, t, X): 83 | p = int(f(k_)) 84 | if N % p == 0: 85 | return p, N // p 86 | 87 | return None 88 | -------------------------------------------------------------------------------- /attacks/factorization/shor.py: -------------------------------------------------------------------------------- 1 | from math import gcd 2 | 3 | from sage.all import divisors 4 | 5 | 6 | def factorize(N, a, s): 7 | """ 8 | Recovers the prime factors from a modulus if the order of a mod n is known. 9 | More information: M. Johnston A., "Shor's Algorithm and Factoring: Don't Throw Away the Odd Orders" 10 | :param N: the modulus 11 | :param a: the base 12 | :param s: the order of a 13 | :return: a tuple containing the prime factors, or None if the factors were not found 14 | """ 15 | assert pow(a, s, N) == 1, "s must be the order of a mod N" 16 | 17 | for r in divisors(s): 18 | b_r = pow(a, s // r, N) 19 | p = gcd(b_r - 1, N) 20 | if 1 < p < N and N % p == 0: 21 | return p, N // p 22 | 23 | return None 24 | -------------------------------------------------------------------------------- /attacks/factorization/twin_primes.py: -------------------------------------------------------------------------------- 1 | from math import isqrt 2 | 3 | 4 | def factorize(N): 5 | """ 6 | Recovers the prime factors from a modulus if the factors are twin primes. 7 | :param N: the modulus 8 | :return: a tuple containing the prime factors, or None if there is no factorization 9 | """ 10 | p = isqrt(N + 1) - 1 11 | q = isqrt(N + 1) + 1 12 | return p, q if p * q == N else None 13 | -------------------------------------------------------------------------------- /attacks/factorization/unbalanced.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | from sage.all import ZZ 6 | 7 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 8 | if sys.path[1] != path: 9 | sys.path.insert(1, path) 10 | 11 | from shared.small_roots import herrmann_may 12 | 13 | 14 | def factorize(N, partial_p, Q, m=1, t=None, check_bounds=True): 15 | """ 16 | Recovers the prime factors from a modulus if the modulus is unbalanced and bits are known. 17 | More information: Brier E. et al., "Factoring Unbalanced Moduli with Known Bits" (Section 4) 18 | :param N: the modulus: N = p * q > q ** 3 19 | :param partial_p: the partial prime factor p (PartialInteger) 20 | :param Q: the bit length of q 21 | :param m: the m value to use for the small roots method (default: 1) 22 | :param t: the t value to use for the small roots method (default: automatically computed using m) 23 | :param check_bounds: perform bounds check (default: True) 24 | :return: a tuple containing the prime factors, or None if the factors could not be found 25 | """ 26 | W = partial_p.get_unknown_lsb() 27 | assert W > 0, "Number of unknown lsb must be greater than 0 (try adding a dummy unknown bit)." 28 | v, L = partial_p.get_known_middle() 29 | assert not check_bounds or 3 * L ** 2 + (4 * W - 6 * Q) * L + 3 * Q ** 2 - 8 * Q * W > 0, f"Bounds check failed ({3 * L ** 2 + (4 * W - 6 * Q) * L + 3 * Q ** 2 - 8 * Q * W} > 0)." 30 | delta = Q / (W + L) 31 | 32 | x, y = ZZ["x", "y"].gens() 33 | a = v * 2 ** W 34 | b = N % (2 ** (W + L)) 35 | f = x * (a + y) - b 36 | X = 2 ** Q 37 | Y = 2 ** W 38 | t = int((1 - 2 * delta) * m) if t is None else t 39 | logging.info(f"Trying {m = }, {t = }...") 40 | for x0, y0 in herrmann_may.modular_bivariate(f, 2 ** (W + L), m, t, X, Y): 41 | q = x0 42 | if q != 0 and N % q == 0: 43 | return N // q, q 44 | 45 | return None 46 | -------------------------------------------------------------------------------- /attacks/gcm/forbidden_attack.py: -------------------------------------------------------------------------------- 1 | from sage.all import GF 2 | 3 | x = GF(2)["x"].gen() 4 | gf2e = GF(2 ** 128, name="y", modulus=x ** 128 + x ** 7 + x ** 2 + x + 1) 5 | 6 | 7 | # Converts an integer to a gf2e element, little endian. 8 | def _to_gf2e(n): 9 | return gf2e([(n >> i) & 1 for i in range(127, -1, -1)]) 10 | 11 | 12 | # Converts a gf2e element to an integer, little endian. 13 | def _from_gf2e(p): 14 | n = p.integer_representation() 15 | ans = 0 16 | for i in range(128): 17 | ans <<= 1 18 | ans |= ((n >> i) & 1) 19 | 20 | return ans 21 | 22 | 23 | # Calculates the GHASH polynomial. 24 | def _ghash(h, a, c): 25 | la = len(a) 26 | lc = len(c) 27 | p = gf2e(0) 28 | for i in range(la // 16): 29 | p += _to_gf2e(int.from_bytes(a[16 * i:16 * (i + 1)], byteorder="big")) 30 | p *= h 31 | 32 | if la % 16 != 0: 33 | p += _to_gf2e(int.from_bytes(a[-(la % 16):] + bytes(16 - la % 16), byteorder="big")) 34 | p *= h 35 | 36 | for i in range(lc // 16): 37 | p += _to_gf2e(int.from_bytes(c[16 * i:16 * (i + 1)], byteorder="big")) 38 | p *= h 39 | 40 | if lc % 16 != 0: 41 | p += _to_gf2e(int.from_bytes(c[-(lc % 16):] + bytes(16 - lc % 16), byteorder="big")) 42 | p *= h 43 | 44 | p += _to_gf2e(((8 * la) << 64) | (8 * lc)) 45 | p *= h 46 | return p 47 | 48 | 49 | def recover_possible_auth_keys(a1, c1, t1, a2, c2, t2): 50 | """ 51 | Recovers possible authentication keys from two messages encrypted with the same authentication key. 52 | More information: Joux A., "Authentication Failures in NIST version of GCM" 53 | :param a1: the associated data of the first message (bytes) 54 | :param c1: the ciphertext of the first message (bytes) 55 | :param t1: the authentication tag of the first message (bytes) 56 | :param a2: the associated data of the second message (bytes) 57 | :param c2: the ciphertext of the second message (bytes) 58 | :param t2: the authentication tag of the second message (bytes) 59 | :return: a generator generating possible authentication keys (gf2e element) 60 | """ 61 | h = gf2e["h"].gen() 62 | p1 = _ghash(h, a1, c1) + _to_gf2e(int.from_bytes(t1, byteorder="big")) 63 | p2 = _ghash(h, a2, c2) + _to_gf2e(int.from_bytes(t2, byteorder="big")) 64 | for h, _ in (p1 + p2).roots(): 65 | yield h 66 | 67 | 68 | def forge_tag(h, a, c, t, target_a, target_c): 69 | """ 70 | Forges an authentication tag for a target message given a message with a known tag. 71 | This method is best used with the authentication keys generated by the recover_possible_auth_keys method. 72 | More information: Joux A., "Authentication Failures in NIST version of GCM" 73 | :param h: the authentication key to use (gf2e element) 74 | :param a: the associated data of the message with the known tag (bytes) 75 | :param c: the ciphertext of the message with the known tag (bytes) 76 | :param t: the known authentication tag (bytes) 77 | :param target_a: the target associated data (bytes) 78 | :param target_c: the target ciphertext (bytes) 79 | :return: the forged authentication tag (bytes) 80 | """ 81 | ghash = _from_gf2e(_ghash(h, a, c)) 82 | target_ghash = _from_gf2e(_ghash(h, target_a, target_c)) 83 | return (ghash ^ int.from_bytes(t, byteorder="big") ^ target_ghash).to_bytes(16, byteorder="big") 84 | -------------------------------------------------------------------------------- /attacks/hnp/extended_hnp.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from sage.all import QQ 5 | from sage.all import ZZ 6 | from sage.all import block_matrix 7 | from sage.all import identity_matrix 8 | from sage.all import matrix 9 | from sage.all import vector 10 | 11 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 12 | if sys.path[1] != path: 13 | sys.path.insert(1, path) 14 | 15 | from shared.lattice import closest_vectors 16 | 17 | 18 | def attack(x_, N, pi, nu, a, p, u, b, delta=None): 19 | """ 20 | Solves the extended hidden number problem (definition 6 in the source paper). 21 | More information: Hlavac M., Rosa T., "Extended Hidden Number Problem and Its Cryptanalytic Applications" (Section 4) 22 | :param x_: the known bits of x 23 | :param N: the modulus 24 | :param pi: the pi values 25 | :param nu: the nu values 26 | :param a: the alpha values 27 | :param p: the rho values 28 | :param u: the mu values 29 | :param b: the beta values 30 | :param delta: the delta value (default: automatically computed) 31 | :return: a generator generating possible values of x 32 | """ 33 | assert len(pi) == len(nu), "pi and v lists should be of equal length." 34 | assert len(a) == len(p) == len(u) == len(b), "a, p, u, and b lists should be of equal length." 35 | 36 | m = len(pi) 37 | d = len(a) 38 | l = [] 39 | for i in range(d): 40 | assert len(p[i]) == len(u[i]), "p[i] and u[i] lists should be of equal length." 41 | l.append(len(p[i])) 42 | 43 | L = sum(l) 44 | D = d + m + L 45 | KD = QQ(2 ** (D / 4) * (m + L) ** (1 / 2) + 1) / 2 46 | delta = QQ(1 / (2 * KD)) if delta is None else QQ(delta) 47 | assert 0 < KD * delta < 1 48 | 49 | Id = identity_matrix(ZZ, d) 50 | P = matrix(ZZ, L, d) 51 | row = 0 52 | for i in range(d): 53 | for j in range(l[i]): 54 | P[row, i] = p[i][j] 55 | row += 1 56 | 57 | A = matrix(ZZ, m, d) 58 | for i in range(d): 59 | for j in range(m): 60 | A[j, i] = a[i] * 2 ** pi[j] 61 | 62 | X = matrix(QQ, m, m) 63 | for j in range(m): 64 | X[j, j] = delta / (2 ** nu[j]) 65 | 66 | K = matrix(QQ, L, L) 67 | pos = 0 68 | for i in range(d): 69 | for j in range(l[i]): 70 | K[pos, pos] = delta / (2 ** u[i][j]) 71 | pos += 1 72 | 73 | B = block_matrix(QQ, [ 74 | [N * Id, matrix(QQ, d, m), matrix(QQ, d, L)], 75 | [A, X, matrix(QQ, m, L)], 76 | [P, matrix(QQ, L, m), K] 77 | ]) 78 | 79 | v = vector(QQ, [delta / 2] * D) 80 | for i in range(d): 81 | v[i] = (b[i] - a[i] * x_) % N 82 | 83 | for W in closest_vectors(B, v, algorithm="babai"): 84 | z = x_ 85 | for j in range(m): 86 | z += 2 ** pi[j] * int((W[d + j] * 2 ** nu[j]) / delta) 87 | z %= N 88 | 89 | yield z 90 | 91 | 92 | def dsa_known_bits(N, h, r, s, x, k): 93 | """ 94 | Recovers the (EC)DSA private key if any nonce bits are known. 95 | :param N: the modulus 96 | :param h: a list containing the hashed messages 97 | :param r: a list containing the r values 98 | :param s: a list containing the s values 99 | :param x: the partial private key (PartialInteger, can be fully unknown) 100 | :param k: a list containing the partial nonces (PartialIntegers) 101 | :return: a generator generating possible private keys 102 | """ 103 | assert len(h) == len(r) == len(s) == len(k), "h, r, s, and k lists should be of equal length." 104 | x_, pi, nu = x.get_known_and_unknowns() 105 | a = [] 106 | p = [] 107 | u = [] 108 | b = [] 109 | for hi, ri, si, ki in zip(h, r, s, k): 110 | a.append(ri) 111 | ki_, li, ui = ki.get_known_and_unknowns() 112 | p.append([(-si * 2 ** lij) % N for lij in li]) 113 | u.append(ui) 114 | b.append((si * ki_ - hi) % N) 115 | 116 | yield from attack(x_, N, pi, nu, a, p, u, b) 117 | -------------------------------------------------------------------------------- /attacks/ige/padding_oracle.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from Crypto.Util.strxor import strxor 4 | 5 | 6 | def _attack_block(padding_oracle, p0, c0, c): 7 | logging.info(f"Attacking block {c.hex()}...") 8 | r = bytes() 9 | for i in reversed(range(16)): 10 | s = bytes([16 - i] * (16 - i)) 11 | for b in range(256): 12 | c0_ = bytes(i) + strxor(s, bytes([b]) + r) 13 | if padding_oracle(p0, c0_, c): 14 | r = bytes([b]) + r 15 | break 16 | else: 17 | raise ValueError(f"Unable to find decryption for {s}, {p0}, {c0}, and {c}") 18 | 19 | return strxor(c0, r) 20 | 21 | 22 | def attack(padding_oracle, p0, c0, c): 23 | """ 24 | Recovers the plaintext using the padding oracle attack. 25 | :param padding_oracle: the padding oracle, returns True if the padding is correct, False otherwise 26 | :param p0: the initial plaintext block 27 | :param c0: the initial ciphertext block 28 | :param c: the ciphertext 29 | :return: the (padded) plaintext 30 | """ 31 | p = _attack_block(padding_oracle, p0, c0, c[0:16]) 32 | for i in range(16, len(c), 16): 33 | p += _attack_block(padding_oracle, p[i - 16:i], c[i - 16:i], c[i:i + 16]) 34 | 35 | return p 36 | -------------------------------------------------------------------------------- /attacks/knapsack/low_density.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from math import ceil 4 | from math import log2 5 | from math import sqrt 6 | 7 | from sage.all import QQ 8 | from sage.all import matrix 9 | 10 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 11 | if sys.path[1] != path: 12 | sys.path.insert(1, path) 13 | 14 | from shared.lattice import shortest_vectors 15 | 16 | 17 | def attack(a, s): 18 | """ 19 | Tries to find e_i values such that sum(e_i * a_i) = s. 20 | This attack only works if the density of the a_i values is < 0.9048. 21 | More information: Coster M. J. et al., "Improved low-density subset sum algorithms" 22 | :param a: the a_i values 23 | :param s: the s value 24 | :return: the e_i values, or None if the e_i values were not found 25 | """ 26 | n = len(a) 27 | d = n / log2(max(a)) 28 | N = ceil(1 / 2 * sqrt(n)) 29 | assert d < 0.9408, f"Density should be less than 0.9408 but was {d}." 30 | 31 | L = matrix(QQ, n + 1, n + 1) 32 | for i in range(n): 33 | L[i, i] = 1 34 | L[i, n] = N * a[i] 35 | 36 | L[n] = [1 / 2] * n + [N * s] 37 | 38 | for v in shortest_vectors(L): 39 | s_ = 0 40 | e = [] 41 | for i in range(n): 42 | ei = 1 - (v[i] + 1 / 2) 43 | if ei != 0 and ei != 1: 44 | break 45 | 46 | ei = int(ei) 47 | s_ += ei * a[i] 48 | e.append(ei) 49 | 50 | if s_ == s: 51 | return e 52 | -------------------------------------------------------------------------------- /attacks/lcg/parameter_recovery.py: -------------------------------------------------------------------------------- 1 | from math import gcd 2 | 3 | from sage.all import GF 4 | from sage.all import is_prime_power 5 | 6 | 7 | def attack(y, m=None, a=None, c=None): 8 | """ 9 | Recovers the parameters from a linear congruential generator. 10 | If no modulus is provided, attempts to recover the modulus from the outputs (may require many outputs). 11 | If no multiplier is provided, attempts to recover the multiplier from the outputs (requires at least 3 outputs). 12 | If no increment is provided, attempts to recover the increment from the outputs (requires at least 2 outputs). 13 | :param y: the sequential output values obtained from the LCG 14 | :param m: the modulus of the LCG (can be None) 15 | :param a: the multiplier of the LCG (can be None) 16 | :param c: the increment of the LCG (can be None) 17 | :return: a tuple containing the modulus, multiplier, and the increment 18 | """ 19 | if m is None: 20 | assert len(y) >= 4, "At least 4 outputs are required to recover the modulus" 21 | for i in range(len(y) - 3): 22 | d0 = y[i + 1] - y[i] 23 | d1 = y[i + 2] - y[i + 1] 24 | d2 = y[i + 3] - y[i + 2] 25 | g = d2 * d0 - d1 * d1 26 | m = g if m is None else gcd(g, m) 27 | 28 | assert is_prime_power(m), "Modulus must be a prime power, try providing more outputs" 29 | 30 | gf = GF(m) 31 | if a is None: 32 | assert len(y) >= 3, "At least 3 outputs are required to recover the multiplier" 33 | x0 = gf(y[0]) 34 | x1 = gf(y[1]) 35 | x2 = gf(y[2]) 36 | a = int((x2 - x1) / (x1 - x0)) 37 | 38 | if c is None: 39 | assert len(y) >= 2, "At least 2 outputs are required to recover the multiplier" 40 | x0 = gf(y[0]) 41 | x1 = gf(y[1]) 42 | c = int(x1 - a * x0) 43 | 44 | return m, a, c 45 | -------------------------------------------------------------------------------- /attacks/lcg/truncated_parameter_recovery.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from itertools import combinations 5 | from math import ceil 6 | from math import gcd 7 | from math import sqrt 8 | 9 | from sage.all import ZZ 10 | from sage.all import Zmod 11 | from sage.all import factor 12 | from sage.all import matrix 13 | 14 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 15 | if sys.path[1] != path: 16 | sys.path.insert(1, path) 17 | 18 | from attacks.hnp import lattice_attack 19 | from shared.lattice import shortest_vectors 20 | from shared.polynomial import polynomial_gcd_crt 21 | 22 | 23 | # Section 2.1 in "On Stern's Attack Against Secret Truncated Linear Congruential Generators". 24 | def _generate_polynomials(y, n, t): 25 | B = matrix(ZZ, n, n + t) 26 | for i in range(n): 27 | for j in range(t): 28 | B[i, j] = y[i + j + 1] - y[i + j] 29 | 30 | B[i, t + i] = 1 31 | 32 | x = ZZ["x"].gen() 33 | for v in shortest_vectors(B): 34 | P = 0 35 | for i, l in enumerate(v[t:]): 36 | P += l * x ** i 37 | yield P 38 | 39 | 40 | # Section 4 in "On Stern's Attack Against Secret Truncated Linear Congruential Generators". 41 | def _recover_modulus_and_multiplier(polynomials, m=None, a=None, check_modulus=None): 42 | for comb in combinations(polynomials, 3): 43 | P0 = comb[0] 44 | P1 = comb[1] 45 | P2 = comb[2] 46 | m_ = gcd(P0.resultant(P1), P1.resultant(P2), P0.resultant(P2)) 47 | if (m is None and check_modulus(m_)) or m_ == m: 48 | if a is None: 49 | factors = factor(m_) 50 | g = polynomial_gcd_crt(P0, polynomial_gcd_crt(P1, P2, factors), factors) 51 | for a_ in g.change_ring(Zmod(m_)).roots(multiplicities=False): 52 | yield int(m_), int(a_) 53 | else: 54 | yield int(m_), a 55 | 56 | 57 | # Generates possible values for the modulus, multiplier, increment, and seed. 58 | # This is similar to the Hidden Number Problem, but with two 'global' unknowns. 59 | def _recover_increment_and_seed(y, k, s, m, a): 60 | a_ = [] 61 | b_ = [] 62 | X = 2 ** (k - s) 63 | mult1 = a 64 | mult2 = 1 65 | for i in range(len(y)): 66 | a_.append([mult1, mult2]) 67 | b_.append(-X * y[i]) 68 | mult1 = (a * mult1) % m 69 | mult2 = (a * mult2 + 1) % m 70 | 71 | for _, (x0_, c_) in lattice_attack.attack(a_, b_, m, X): 72 | yield m, a, c_, x0_ 73 | 74 | 75 | def attack(y, k, s, m=None, a=None, check_modulus=None): 76 | """ 77 | Recovers possible parameters and states from a truncated linear congruential generator. 78 | More information: Contini S., Shparlinski I. E., "On Stern's Attack Against Secret Truncated Linear Congruential Generators" 79 | If no modulus is provided, attempts to recover a modulus from the outputs. 80 | If no multiplier is provided, attempts to recover a multiplier from the outputs. 81 | Also recovers an increment from the outputs. 82 | The resulting parameters may not match the original parameters, but the generated sequence should be the same up to some small error. 83 | :param y: the sequential output values obtained from the truncated LCG (the states truncated to s most significant bits) 84 | :param k: the bit length of the states 85 | :param s: the bit length of the outputs 86 | :param m: the modulus of the LCG (can be None) 87 | :param a: the multiplier of the LCG (can be None) 88 | :param check_modulus: a function which checks if a possible value can be the modulus (default: compare the bit length with k) 89 | :return: a generator generating possible parameters (tuples of modulus, multiplier, increment, and seed) of the truncated LCG 90 | """ 91 | if m is None or a is None: 92 | alpha = s / k 93 | t = int(1 / alpha) 94 | n = ceil(sqrt(2 * alpha * t * k)) 95 | 96 | # We start at the minimum useful chunk size. 97 | chunk_size = n + t 98 | while chunk_size <= len(y): 99 | logging.info(f"Trying chunk size {chunk_size}...") 100 | polynomials = [] 101 | for i in range(len(y) // chunk_size): 102 | logging.info(f"Generating polynomials for {n = }, {t = }...") 103 | for P in _generate_polynomials(y[chunk_size * i:chunk_size * (i + 1)], n, t): 104 | polynomials.append(P) 105 | 106 | logging.info("Recovering modulus and multiplier...") 107 | for m_, a_ in _recover_modulus_and_multiplier(polynomials, m, a, check_modulus or (lambda m_: m_.bit_length() == k)): 108 | logging.info("Recovering increment and seed...") 109 | yield from _recover_increment_and_seed(y, k, s, m_, a_) 110 | 111 | t += 1 112 | n = ceil(sqrt(2 * alpha * t * k)) 113 | chunk_size = n + t 114 | else: 115 | logging.info("Recovering increment and seed...") 116 | yield from _recover_increment_and_seed(y, k, s, m, a) 117 | -------------------------------------------------------------------------------- /attacks/lcg/truncated_state_recovery.py: -------------------------------------------------------------------------------- 1 | from sage.all import QQ 2 | from sage.all import ZZ 3 | from sage.all import matrix 4 | from sage.all import vector 5 | 6 | 7 | def attack(y, k, s, m, a, c): 8 | """ 9 | Recovers the states associated with the outputs from a truncated linear congruential generator. 10 | More information: Frieze, A. et al., "Reconstructing Truncated Integer Variables Satisfying Linear Congruences" 11 | :param y: the sequential output values obtained from the truncated LCG (the states truncated to s most significant bits) 12 | :param k: the bit length of the states 13 | :param s: the bit length of the outputs 14 | :param m: the modulus of the LCG 15 | :param a: the multiplier of the LCG 16 | :param c: the increment of the LCG 17 | :return: a list containing the states associated with the provided outputs 18 | """ 19 | diff_bit_length = k - s 20 | 21 | # Preparing for the lattice reduction. 22 | delta = c % m 23 | y = vector(ZZ, y) 24 | for i in range(len(y)): 25 | # Shift output value to the MSBs and remove the increment. 26 | y[i] = (y[i] << diff_bit_length) - delta 27 | delta = (a * delta + c) % m 28 | 29 | # This lattice only works for increment = 0. 30 | B = matrix(ZZ, len(y), len(y)) 31 | B[0, 0] = m 32 | for i in range(1, len(y)): 33 | B[i, 0] = a ** i 34 | B[i, i] = -1 35 | 36 | B = B.LLL() 37 | 38 | # Finding the target value to solve the equation for the states. 39 | b = B * y 40 | for i in range(len(b)): 41 | b[i] = round(QQ(b[i]) / m) * m - b[i] 42 | 43 | # Recovering the states 44 | delta = c % m 45 | x = list(B.solve_right(b)) 46 | for i, state in enumerate(x): 47 | # Adding the MSBs and the increment back again. 48 | x[i] = int(y[i] + state + delta) 49 | delta = (a * delta + c) % m 50 | 51 | return x 52 | -------------------------------------------------------------------------------- /attacks/lwe/arora_ge.py: -------------------------------------------------------------------------------- 1 | from sage.all import GF 2 | 3 | 4 | def attack(q, A, b, E, S=None): 5 | """ 6 | Recovers the secret key s from the LWE samples A and b. 7 | More information: "The Learning with Errors Problem: Algorithms" (Section 1) 8 | :param q: the modulus 9 | :param A: the matrix A, represented as a list of lists 10 | :param b: the vector b, represented as a list 11 | :param E: the possible error values 12 | :param S: the possible values of the entries in s (default: None) 13 | :return: a list representing the secret key s 14 | """ 15 | m = len(A) 16 | n = len(A[0]) 17 | gf = GF(q) 18 | pr = gf[tuple(f"x{i}" for i in range(n))] 19 | gens = pr.gens() 20 | 21 | f = [] 22 | for i in range(m): 23 | p = 1 24 | for e in E: 25 | p *= (b[i] - sum(A[i][j] * gens[j] for j in range(n)) - e) 26 | f.append(p) 27 | 28 | if S is not None: 29 | # Use information about the possible values for s to add more polynomials. 30 | for j in range(n): 31 | p = 1 32 | for s in S: 33 | p *= (gens[j] - s) 34 | f.append(p) 35 | 36 | s = [] 37 | for p in pr.ideal(f).groebner_basis(): 38 | assert p.nvariables() == 1 and p.degree() == 1 39 | s.append(int(-p.constant_coefficient())) 40 | 41 | return s 42 | -------------------------------------------------------------------------------- /attacks/mersenne_twister/__init__.py: -------------------------------------------------------------------------------- 1 | class MersenneTwister: 2 | def __init__(self, w, n, m, r, a, b, c, s, t, u, d, l): 3 | self.w = w 4 | self.n = n 5 | self.m = m 6 | self.a = a 7 | self.b = b 8 | self.c = c 9 | self.s = s 10 | self.t = t 11 | self.u = u 12 | self.d = d 13 | self.l = l 14 | self.mt = [0] * n 15 | self.index = n + 1 16 | self.lower_mask = (1 << r) - 1 17 | self.upper_mask = (~self.lower_mask) % (2 ** self.w) 18 | 19 | def seed(self, f, seed): 20 | self.index = self.n 21 | self.mt[0] = seed 22 | for i in range(1, self.n): 23 | self.mt[i] = (f * (self.mt[i - 1] ^ (self.mt[i - 1] >> (self.w - 2))) + i) % (2 ** self.w) 24 | 25 | def _twist(self): 26 | for i in range(self.n): 27 | x = (self.mt[i] & self.upper_mask) + (self.mt[(i + 1) % self.n] & self.lower_mask) 28 | xA = x >> 1 29 | if x % 2 != 0: 30 | xA ^= self.a 31 | self.mt[i] = self.mt[(i + self.m) % self.n] ^ xA 32 | self.index = 0 33 | 34 | def __next__(self): 35 | if self.index >= self.n: 36 | if self.index > self.n: 37 | raise ValueError("Generator was never seeded") 38 | self._twist() 39 | 40 | y = self.mt[self.index] 41 | y ^= (y >> self.u) & self.d 42 | y ^= (y << self.s) & self.b 43 | y ^= (y << self.t) & self.c 44 | y ^= y >> self.l 45 | self.index += 1 46 | return y % (2 ** self.w) 47 | 48 | 49 | def mt19937(): 50 | """ 51 | Constructs a new unseeded MT19937 instance. 52 | :return: the new MT19937 instance 53 | """ 54 | w = 32 55 | n = 624 56 | m = 397 57 | r = 31 58 | a = 0x9908B0DF 59 | b = 0x9D2C5680 60 | c = 0xEFC60000 61 | s = 7 62 | t = 15 63 | u = 11 64 | d = 0xFFFFFFFF 65 | l = 18 66 | return MersenneTwister(w, n, m, r, a, b, c, s, t, u, d, l) 67 | 68 | 69 | def mt19937_64(): 70 | """ 71 | Constructs a new unseeded MT19937-64 instance. 72 | :return: the new MT19937-64 instance 73 | """ 74 | w = 64 75 | n = 312 76 | m = 156 77 | r = 31 78 | a = 0xB5026F5AA96619E9 79 | b = 0x71D67FFFEDA60000 80 | c = 0xFFF7EEE000000000 81 | s = 17 82 | t = 37 83 | u = 29 84 | d = 0x5555555555555555 85 | l = 43 86 | return MersenneTwister(w, n, m, r, a, b, c, s, t, u, d, l) 87 | -------------------------------------------------------------------------------- /attacks/mersenne_twister/state_recovery.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 5 | if sys.path[1] != path: 6 | sys.path.insert(1, path) 7 | 8 | from attacks import mersenne_twister 9 | 10 | 11 | def _reverse_left(y, shift, mask, w): 12 | y_ = 0 13 | for i in range(shift, w, shift): 14 | m = 2 ** i - 1 15 | y_ = (y ^ ((y_ << shift) & mask)) & m 16 | y_ = (y ^ ((y_ << shift) & mask)) & (2 ** w - 1) 17 | return y_ 18 | 19 | 20 | def _reverse_right(y, shift, mask, w): 21 | y_ = 0 22 | for i in range(shift, w, shift): 23 | m = (2 ** i - 1) << (w - i) 24 | y_ = (y ^ ((y_ >> shift) & mask)) & m 25 | y_ = (y ^ ((y_ >> shift) & mask)) & (2 ** w - 1) 26 | return y_ 27 | 28 | 29 | def _attack_mt(y, mt): 30 | assert len(y) == mt.n 31 | mt.index = 0 32 | while mt.index < mt.n: 33 | yi = y[mt.index] 34 | yi = _reverse_right(yi, mt.l, 2 ** mt.w - 1, mt.w) 35 | yi = _reverse_left(yi, mt.t, mt.c, mt.w) 36 | yi = _reverse_left(yi, mt.s, mt.b, mt.w) 37 | yi = _reverse_right(yi, mt.u, mt.d, mt.w) 38 | mt.mt[mt.index] = yi 39 | mt.index += 1 40 | return mt 41 | 42 | 43 | def attack(y, w, n, m, r, a, b, c, s, t, u, d, l): 44 | """ 45 | Recovers the state from a Mersenne Twister instance using n outputs. 46 | No twist should have been performed during the outputs. 47 | :param y: the outputs (must be of length n) 48 | :param w: the parameter w 49 | :param n: the parameter n 50 | :param m: the parameter m 51 | :param r: the parameter r 52 | :param a: the parameter a 53 | :param b: the parameter b 54 | :param c: the parameter c 55 | :param s: the parameter s 56 | :param t: the parameter t 57 | :param u: the parameter u 58 | :param d: the parameter d 59 | :param l: the parameter l 60 | :return: a cloned Mersenne Twister instance 61 | """ 62 | return _attack_mt(y, mersenne_twister.MersenneTwister(w, n, m, r, a, b, c, s, t, u, d, l)) 63 | 64 | 65 | def attack_mt19937(y): 66 | """ 67 | Recovers the state from an MT19937 instance using 624 outputs. 68 | No twist should have been performed during the outputs. 69 | :param y: the outputs 70 | :return: a cloned MT19937 instance 71 | """ 72 | return _attack_mt(y, mersenne_twister.mt19937()) 73 | 74 | 75 | def attack_mt19937_64(y): 76 | """ 77 | Recovers the state from an MT19937-64 instance using 312 outputs. 78 | No twist should have been performed during the outputs. 79 | :param y: the outputs 80 | :return: a cloned MT19937-64 instance 81 | """ 82 | return _attack_mt(y, mersenne_twister.mt19937_64()) 83 | -------------------------------------------------------------------------------- /attacks/otp/key_reuse.py: -------------------------------------------------------------------------------- 1 | from math import log10 2 | import string 3 | 4 | 5 | def _hamming_distance(a, b): 6 | distance = 0 7 | for x, y in zip(a, b): 8 | distance += bin(x ^ y).count("1") 9 | 10 | return distance 11 | 12 | 13 | def _guess_key_sizes(c: list[bytes], max_key_size): 14 | key_sizes = [] 15 | prev_distance = None 16 | for key_size in range(2, max_key_size + 1): 17 | blocks = [] 18 | for ci in c: 19 | j = 0 20 | while (j + 1) * key_size <= len(ci): 21 | blocks.append(ci[j * key_size:(j + 1) * key_size]) 22 | j += 1 23 | 24 | if len(blocks) < 2: 25 | continue 26 | 27 | distance = 0 28 | for i in range(len(blocks) - 1): 29 | distance += _hamming_distance(blocks[i], blocks[i + 1]) 30 | 31 | distance /= len(blocks) - 1 32 | distance /= key_size 33 | if prev_distance is not None: 34 | diff = prev_distance - distance 35 | 36 | key_sizes.append((key_size, diff)) 37 | 38 | prev_distance = distance 39 | 40 | return [x[0] for x in sorted(key_sizes, key=lambda x: x[1], reverse=True)] 41 | 42 | 43 | def _score(p, char_frequencies, char_floor): 44 | score = 0 45 | for b in p: 46 | c = chr(b) 47 | if not (c in string.printable or c in string.whitespace): 48 | return None 49 | 50 | c = c.lower() 51 | if c in char_frequencies: 52 | score += log10(char_frequencies[c]) 53 | else: 54 | score += char_floor 55 | 56 | return score 57 | 58 | 59 | def _transpose(c, i, key_size): 60 | transposed = bytearray() 61 | for c in c: 62 | j = 0 63 | while j + i < len(c): 64 | transposed.append(c[j + i]) 65 | j += key_size 66 | 67 | return transposed 68 | 69 | 70 | def _frequency_analysis(c, char_frequencies, char_floor): 71 | max_score = float("-inf") 72 | candidate_k = None 73 | for k in range(256): 74 | p = bytes([b ^ k for b in c]) 75 | score = _score(p, char_frequencies, char_floor) 76 | if score is not None and score > max_score: 77 | max_score = score 78 | candidate_k = k 79 | 80 | return candidate_k 81 | 82 | 83 | def attack(c, char_frequencies, char_floor, key_size=None): 84 | """ 85 | Breaks the one-time pad when the key is reused in a single plaintext or multiple plaintexts. 86 | Note: this implementation is very primitive and only for educational purposes. For more real-world analysis, use xortool. 87 | :param c: the list of ciphertexts 88 | :param char_frequencies: a dict of (char, frequency) items for the plaintext language 89 | :param char_floor: the value to assign to a character if it is not found in char_frequencies 90 | :param key_size: the size of the key in bytes (default: None): if no key size is given, this method attempts to discover it using the Hamming distance 91 | :return: the best guess for the key 92 | """ 93 | if key_size is None: 94 | key_sizes = _guess_key_sizes(c, max(map(lambda ci: len(ci), c))) 95 | else: 96 | key_sizes = [key_size] 97 | 98 | for key_size in key_sizes: 99 | k = bytearray(key_size) 100 | for i in range(key_size): 101 | transposed = _transpose(c, i, key_size) 102 | candidate_k = _frequency_analysis(transposed, char_frequencies, char_floor) 103 | if candidate_k is None: 104 | break 105 | else: 106 | k[i] = candidate_k 107 | else: 108 | return k 109 | -------------------------------------------------------------------------------- /attacks/pseudoprimes/miller_rabin.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from math import gcd 5 | 6 | from sage.all import is_prime 7 | from sage.all import kronecker 8 | from sage.all import next_prime 9 | 10 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 11 | if sys.path[1] != path: 12 | sys.path.insert(1, path) 13 | 14 | from shared.crt import fast_crt 15 | 16 | 17 | def _generate_s(A, k): 18 | S = [] 19 | for a in A: 20 | # Possible non-residues mod 4a of potential primes p 21 | Sa = set() 22 | for p in range(1, 4 * a, 2): 23 | if kronecker(a, p) == -1: 24 | Sa.add(p) 25 | 26 | # Subsets of Sa that meet the intersection requirement 27 | Sk = [] 28 | for ki in k: 29 | assert gcd(ki, 4 * a) == 1 30 | Sk.append({pow(ki, -1, 4 * a) * (s + ki - 1) % (4 * a) for s in Sa}) 31 | 32 | S.append(Sa.intersection(*Sk)) 33 | 34 | return S 35 | 36 | 37 | # Brute forces a combination of residues from S by backtracking 38 | # X already contains the remainders mod each k 39 | # M already contains each k 40 | def _backtrack(S, A, X, M, i): 41 | if i == len(S): 42 | return fast_crt(X, M) 43 | 44 | M.append(4 * A[i]) 45 | for za in S[i]: 46 | X.append(za) 47 | try: 48 | fast_crt(X, M) 49 | z, m = _backtrack(S, A, X, M, i + 1) 50 | if z is not None and m is not None: 51 | return z, m 52 | except ValueError: 53 | pass 54 | X.pop() 55 | 56 | M.pop() 57 | return None, None 58 | 59 | 60 | def generate_pseudoprime(A, k2=None, k3=None, min_bit_length=0): 61 | """ 62 | Generates a pseudoprime of the form p1 * p2 * p3 which passes the Miller-Rabin primality test for the provided bases. 63 | More information: R. Albrecht M. et al., "Prime and Prejudice: Primality Testing Under Adversarial Conditions" 64 | :param A: the bases 65 | :param k2: the k2 value (default: next_prime(A[-1])) 66 | :param k3: the k3 value (default: next_prime(k2)) 67 | :param min_bit_length: the minimum bit length of the generated pseudoprime (default: 0) 68 | :return: a tuple containing the pseudoprime n, as well as its 3 prime factors 69 | """ 70 | A.sort() 71 | if k2 is None: 72 | k2 = int(next_prime(A[-1])) 73 | if k3 is None: 74 | k3 = int(next_prime(k2)) 75 | while True: 76 | logging.info(f"Trying {k2 = } and {k3 = }...") 77 | X = [pow(-k3, -1, k2), pow(-k2, -1, k3)] 78 | M = [k2, k3] 79 | S = _generate_s(A, M) 80 | logging.info(f"{S = }") 81 | z, m = _backtrack(S, A, X, M, 0) 82 | if z and m: 83 | logging.info(f"Found residue {z} and modulus {m}") 84 | i = (2 ** (min_bit_length // 3)) // m 85 | while True: 86 | p1 = int(z + i * m) 87 | p2 = k2 * (p1 - 1) + 1 88 | p3 = k3 * (p1 - 1) + 1 89 | if is_prime(p1) and is_prime(p2) and is_prime(p3): 90 | return p1 * p2 * p3, p1, p2, p3 91 | 92 | i += 1 93 | else: 94 | k3 = int(next_prime(k3)) 95 | -------------------------------------------------------------------------------- /attacks/rc4/fms.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | 4 | def _possible_key_bit(key, c): 5 | s = [i for i in range(256)] 6 | j = 0 7 | for i in range(len(key)): 8 | j = (j + s[i] + key[i]) % 256 9 | tmp = s[i] 10 | s[i] = s[j] 11 | s[j] = tmp 12 | 13 | return (c[0] - j - s[len(key)]) % 256 14 | 15 | 16 | def attack(encrypt_oracle, key_len): 17 | """ 18 | Recovers the hidden part of an RC4 key using the Fluhrer-Mantin-Shamir attack. 19 | :param encrypt_oracle: the padding oracle, returns the encryption of a plaintext under a hidden key concatenated with the iv 20 | :param key_len: the length of the hidden part of the key 21 | :return: the hidden part of the key 22 | """ 23 | key = bytearray([3, 255, 0]) 24 | for a in range(key_len): 25 | key[0] = a + 3 26 | possible = Counter() 27 | for x in range(256): 28 | key[2] = x 29 | c = encrypt_oracle(key[:3], b"\x00") 30 | possible[_possible_key_bit(key, c)] += 1 31 | key.append(possible.most_common(1)[0][0]) 32 | 33 | return key[3:] 34 | -------------------------------------------------------------------------------- /attacks/rsa/bleichenbacher.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from random import randrange 5 | 6 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 7 | if sys.path[1] != path: 8 | sys.path.insert(1, path) 9 | 10 | from shared import ceil_div 11 | from shared import floor_div 12 | 13 | 14 | def _insert(M, a, b): 15 | for i, (a_, b_) in enumerate(M): 16 | if a_ <= b and a <= b_: 17 | a = min(a, a_) 18 | b = max(b, b_) 19 | M[i] = (a, b) 20 | return 21 | 22 | M.append((a, b)) 23 | return 24 | 25 | 26 | # Step 1. 27 | def _step_1(padding_oracle, n, e, c): 28 | s0 = 1 29 | c0 = c 30 | while not padding_oracle(c0): 31 | s0 = randrange(2, n) 32 | c0 = (c * pow(s0, e, n)) % n 33 | 34 | return s0, c0 35 | 36 | 37 | # Step 2.a. 38 | def _step_2a(padding_oracle, n, e, c0, B): 39 | s = ceil_div(n, 3 * B) 40 | while not padding_oracle((c0 * pow(s, e, n)) % n): 41 | s += 1 42 | 43 | return s 44 | 45 | 46 | # Step 2.b. 47 | def _step_2b(padding_oracle, n, e, c0, s): 48 | s += 1 49 | while not padding_oracle((c0 * pow(s, e, n)) % n): 50 | s += 1 51 | 52 | return s 53 | 54 | 55 | # Step 2.c. 56 | def _step_2c(padding_oracle, n, e, c0, B, s, a, b): 57 | r = ceil_div(2 * (b * s - 2 * B), n) 58 | while True: 59 | left = ceil_div(2 * B + r * n, b) 60 | right = floor_div(3 * B + r * n, a) 61 | for s in range(left, right + 1): 62 | if padding_oracle((c0 * pow(s, e, n)) % n): 63 | return s 64 | 65 | r += 1 66 | 67 | 68 | # Step 3. 69 | def _step_3(n, B, s, M): 70 | M_ = [] 71 | for (a, b) in M: 72 | left = ceil_div(a * s - 3 * B + 1, n) 73 | right = floor_div(b * s - 2 * B, n) 74 | for r in range(left, right + 1): 75 | a_ = max(a, ceil_div(2 * B + r * n, s)) 76 | b_ = min(b, floor_div(3 * B - 1 + r * n, s)) 77 | _insert(M_, a_, b_) 78 | 79 | return M_ 80 | 81 | 82 | def attack(padding_oracle, n, e, c): 83 | """ 84 | Recovers the plaintext using Bleichenbacher's attack. 85 | More information: Bleichenbacher D., "Chosen Ciphertext Attacks Against Protocols Based on the RSA Encryption Standard PKCS #1" 86 | :param padding_oracle: the padding oracle taking integers, returns True if the PKCS #1 v1.5 padding is correct, False otherwise 87 | :param n: the modulus 88 | :param e: the public exponent 89 | :param c: the ciphertext (integer) 90 | :return: the plaintext (integer) 91 | """ 92 | k = ceil_div(n.bit_length(), 8) 93 | B = 2 ** (8 * (k - 2)) 94 | logging.info("Executing step 1...") 95 | s0, c0 = _step_1(padding_oracle, n, e, c) 96 | M = [(2 * B, 3 * B - 1)] 97 | logging.info("Executing step 2.a...") 98 | s = _step_2a(padding_oracle, n, e, c0, B) 99 | M = _step_3(n, B, s, M) 100 | logging.info("Starting while loop...") 101 | while True: 102 | if len(M) > 1: 103 | s = _step_2b(padding_oracle, n, e, c0, s) 104 | else: 105 | (a, b) = M[0] 106 | if a == b: 107 | m = (a * pow(s0, -1, n)) % n 108 | return m 109 | s = _step_2c(padding_oracle, n, e, c0, B, s, a, b) 110 | M = _step_3(n, B, s, M) 111 | -------------------------------------------------------------------------------- /attacks/rsa/bleichenbacher_signature_forgery.py: -------------------------------------------------------------------------------- 1 | def attack(suffix, suffix_bit_length): 2 | """ 3 | Returns a number s for which s^3 ends with the provided suffix. 4 | :param suffix: the suffix 5 | :param suffix_bit_length: the bit length of the suffix 6 | :return: the number s 7 | """ 8 | assert suffix % 2 == 1, "Target suffix must be odd" 9 | 10 | s = 1 11 | for i in range(suffix_bit_length): 12 | if (((s ** 3) >> i) & 1) != ((suffix >> i) & 1): 13 | s |= (1 << i) 14 | 15 | return s 16 | -------------------------------------------------------------------------------- /attacks/rsa/boneh_durfee.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | from sage.all import RR 6 | from sage.all import ZZ 7 | 8 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 9 | if sys.path[1] != path: 10 | sys.path.insert(1, path) 11 | 12 | from attacks.factorization import known_phi 13 | from shared.small_roots import herrmann_may 14 | 15 | 16 | def attack(N, e, factor_bit_length, partial_p=None, delta=0.25, m=1, t=None): 17 | """ 18 | Recovers the prime factors if the private exponent is too small. 19 | This implementation exploits knowledge of least significant bits of prime factors, if available. 20 | More information: Boneh D., Durfee G., "Cryptanalysis of RSA with Private Key d Less than N^0.292" 21 | :param N: the modulus 22 | :param e: the public exponent 23 | :param factor_bit_length: the bit length of the prime factors 24 | :param partial_p: the partial prime factor p (PartialInteger) (default: None) 25 | :param delta: a predicted bound on the private exponent (d < N^delta) (default: 0.25) 26 | :param m: the m value to use for the small roots method (default: 1) 27 | :param t: the t value to use for the small roots method (default: automatically computed using m) 28 | :return: a tuple containing the prime factors, or None if the factors were not found 29 | """ 30 | # Use additional information about factors to speed up Boneh-Durfee. 31 | p_lsb, p_lsb_bit_length = (0, 0) if partial_p is None else partial_p.get_known_lsb() 32 | q_lsb = (pow(p_lsb, -1, 2 ** p_lsb_bit_length) * N) % (2 ** p_lsb_bit_length) 33 | A = ((N >> p_lsb_bit_length) + pow(2, -p_lsb_bit_length, e) * (p_lsb * q_lsb - p_lsb - q_lsb + 1)) 34 | 35 | x, y = ZZ["x", "y"].gens() 36 | f = x * (A + y) + pow(2, -p_lsb_bit_length, e) 37 | X = int(RR(e) ** delta) 38 | Y = int(2 ** (factor_bit_length - p_lsb_bit_length + 1)) 39 | t = int((1 - 2 * delta) * m) if t is None else t 40 | logging.info(f"Trying {m = }, {t = }...") 41 | for x0, y0 in herrmann_may.modular_bivariate(f, e, m, t, X, Y): 42 | z = int(f(x0, y0)) 43 | if z % e == 0: 44 | k = pow(x0, -1, e) 45 | s = (N + 1 + k) % e 46 | phi = N - s + 1 47 | factors = known_phi.factorize(N, phi) 48 | if factors: 49 | return factors 50 | 51 | return None 52 | 53 | 54 | def attack_multi_prime(N, e, factor_bit_length, factors, delta=0.25, m=1, t=None): 55 | """ 56 | Recovers the prime factors if the private exponent is too small. 57 | This method works for a modulus consisting of any number of primes. 58 | :param N: the modulus 59 | :param e: the public exponent 60 | :param factor_bit_length: the bit length of the prime factors 61 | :param factors: the number of prime factors in the modulus 62 | :param delta: a predicted bound on the private exponent (d < n^delta) (default: 0.25) 63 | :param m: the m value to use for the small roots method (default: 1) 64 | :param t: the t value to use for the small roots method (default: automatically computed using m) 65 | :return: a tuple containing the prime factors, or None if the factors were not found 66 | """ 67 | x, y = ZZ["x", "y"].gens() 68 | A = N + 1 69 | f = x * (A + y) + 1 70 | X = int(RR(e) ** delta) 71 | Y = int(2 ** ((factors - 1) * factor_bit_length + 1)) 72 | t = int((1 - 2 * delta) * m) if t is None else t 73 | logging.info(f"Trying {m = }, {t = }...") 74 | for x0, y0 in herrmann_may.modular_bivariate(f, e, m, t, X, Y): 75 | z = int(f(x0, y0)) 76 | if z % e == 0: 77 | k = pow(x0, -1, e) 78 | s = (N + 1 + k) % e 79 | phi = N - s + 1 80 | factors = known_phi.factorize_multi_prime(N, phi) 81 | if factors: 82 | return factors 83 | 84 | return None 85 | -------------------------------------------------------------------------------- /attacks/rsa/cherkaoui_semmouni.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from math import isqrt 5 | from math import log 6 | from math import sqrt 7 | 8 | from sage.all import RR 9 | from sage.all import ZZ 10 | 11 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 12 | if sys.path[1] != path: 13 | sys.path.insert(1, path) 14 | 15 | from shared.small_roots import herrmann_may 16 | 17 | 18 | def attack(N, e, beta, delta, m=1, t=None, check_bounds=True): 19 | """ 20 | Recovers the prime factors of a modulus and the private exponent if |p - q| is sufficiently small. 21 | More information: Cherkaoui-Semmouni M. et al., "Cryptanalysis of RSA Variants with Primes Sharing Most Significant Bits" 22 | :param N: the modulus 23 | :param e: the exponent 24 | :param beta: the parameter beta such that |p - q| <= N^beta 25 | :param delta: the parameter delta such that d <= N^delta 26 | :param m: the m value to use for the small roots method (default: 1) 27 | :param t: the t value to use for the small roots method (default: automatically computed using m) 28 | :param check_bounds: perform bounds check (default: True) 29 | :return: a tuple containing the prime factors and the private exponent, or None if the factors could not be found 30 | """ 31 | alpha = log(e, N) 32 | assert not check_bounds or delta < 2 - sqrt(2 * alpha * beta), f"Bounds check failed ({delta} < {2 - sqrt(2 * alpha * beta)})." 33 | 34 | x, y = ZZ["x", "y"].gens() 35 | A = -(N - 1) ** 2 36 | f = x * y + A * x + 1 37 | X = int(2 * e * RR(N) ** (delta - 2)) # Equivalent to 2N^(alpha + delta - 2) 38 | Y = int(RR(N) ** (2 * beta)) 39 | t = int((2 - delta - 2 * beta) / (2 * beta) * m) if t is None else t 40 | logging.info(f"Trying {m = }, {t = }...") 41 | for x0, y0 in herrmann_may.modular_bivariate(f, e, m, t, X, Y): 42 | s = isqrt(y0) 43 | d = s ** 2 + 4 * N 44 | p = int(-s + isqrt(d)) // 2 45 | q = int(s + isqrt(d)) // 2 46 | d = int(f(x0, y0) // e) 47 | return p, q, d 48 | 49 | return None 50 | -------------------------------------------------------------------------------- /attacks/rsa/common_modulus.py: -------------------------------------------------------------------------------- 1 | from sage.all import ZZ 2 | from sage.all import xgcd 3 | 4 | 5 | def attack(n, e1, c1, e2, c2): 6 | """ 7 | Recovers the plaintext from two ciphertexts, encrypted using the same modulus and different public exponents. 8 | :param n: the common modulus 9 | :param e1: the first public exponent 10 | :param c1: the ciphertext of the first encryption 11 | :param e2: the second public exponent 12 | :param c2: the ciphertext of the second encryption 13 | :return: the plaintext 14 | """ 15 | g, u, v = xgcd(e1, e2) 16 | p1 = pow(c1, u, n) if u > 0 else pow(pow(c1, -1, n), -u, n) 17 | p2 = pow(c2, v, n) if v > 0 else pow(pow(c2, -1, n), -v, n) 18 | return int(ZZ(int(p1 * p2) % n).nth_root(g)) 19 | -------------------------------------------------------------------------------- /attacks/rsa/crt_fault_attack.py: -------------------------------------------------------------------------------- 1 | from math import gcd 2 | 3 | 4 | def attack_known_m(n, e, m, s): 5 | """ 6 | Recovers the prime factors from a modulus using a known message and its faulty signature. 7 | :param n: the modulus 8 | :param e: the public exponent 9 | :param m: the message 10 | :param s: the faulty signature 11 | :return: a tuple containing the prime factors, or None if the signature wasn't actually faulty 12 | """ 13 | g = gcd(m - pow(s, e, n), n) 14 | return None if g == 1 else (g, n // g) 15 | 16 | 17 | def attack_unknown_m(n, e, sv, sf): 18 | """ 19 | Recovers the prime factors from a modulus using a correct valid and a faulty signature from the same (unknown) message. 20 | :param n: the modulus 21 | :param e: the public exponent 22 | :param sv: the valid signature 23 | :param sf: the faulty signature 24 | :return: a tuple containing the prime factors, or None if the signatures were both valid, or both faulty 25 | """ 26 | assert sv != sf 27 | g = gcd(sv - sf, n) 28 | return None if g == 1 else (g, n // g) 29 | -------------------------------------------------------------------------------- /attacks/rsa/d_fault_attack.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 5 | if sys.path[1] != path: 6 | sys.path.insert(1, path) 7 | 8 | from shared.partial_integer import PartialInteger 9 | 10 | 11 | def attack(n, e, sv, sf): 12 | """ 13 | Recovers the bits of the private exponent d that were flipped during generation of signatures. 14 | More faulty signatures reveal more bits of d, assuming the bit flip positions are different. 15 | :param n: the modulus 16 | :param e: the public exponent 17 | :param sv: the valid signature 18 | :param sf: the list of faulty signatures: for each entry in this list, at most one bit in d should have been flipped during signature generation 19 | :return: a PartialInteger containing the known and unknown bits of d 20 | """ 21 | d_bits = [None] * n.bit_length() 22 | m = 2 23 | mi = {pow(m, 2 ** i, n): i for i in range(n.bit_length())} 24 | for sfi in sf: 25 | di0 = pow(sv, -1, n) * sfi % n 26 | di1 = sv * pow(sfi, -1, n) % n 27 | if di0 in mi: 28 | d_bits[mi[di0]] = 0 29 | if di1 in mi: 30 | d_bits[mi[di1]] = 1 31 | 32 | return PartialInteger.from_bits_le(d_bits) 33 | -------------------------------------------------------------------------------- /attacks/rsa/desmedt_odlyzko.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sage.all import GF 4 | from sage.all import ZZ 5 | from sage.all import factor 6 | from sage.all import matrix 7 | from sage.all import next_prime 8 | from sage.all import vector 9 | 10 | 11 | def attack(hash_oracle, sign_oracle, N, e, target_m): 12 | """ 13 | Performs a selective forgery attack using the Desmedt-Odlyzko attack. 14 | Note that this selective forgery attack is much slower than the existential forgery attack. However, it is also more applicable to real world scenarios. 15 | More information: Coron J. et al., "Practical Cryptanalysis of ISO 9796-2 and EMV Signatures (Section 3)" 16 | :param hash_oracle: the oracle taking integer messages, returns an integer representation of the hashed message 17 | :param sign_oracle: the oracle taking integer messages, returns an integer representation of the signature 18 | :param N: the modulus 19 | :param e: the public exponent 20 | :param target_m: the target message to sign (integer) 21 | :return: the signature of the target message (integer) 22 | """ 23 | target_factors = factor(hash_oracle(target_m)) 24 | B, _ = target_factors[-1] 25 | 26 | logging.info(f"Computing all primes <= {B}...") 27 | primes = {} 28 | p = 2 29 | i = 0 30 | while p <= B: 31 | primes[p] = i 32 | p = next_prime(p) 33 | i += 1 34 | 35 | l = len(primes) 36 | 37 | Vt = vector(GF(e), l, sparse=True) 38 | for p, v in target_factors: 39 | Vt[primes[p]] = v 40 | 41 | logging.info(f"Generating initial {l}x{l} matrix...") 42 | M = matrix(GF(e), l, sparse=True) 43 | m = [] 44 | mi = 0 45 | i = 0 46 | while i < l: 47 | factors = factor(hash_oracle(mi)) 48 | if all(p <= B for p, _ in factors): 49 | for p, v in factors: 50 | M[i, primes[p]] = v 51 | m.append(mi) 52 | i += 1 53 | mi += 1 54 | 55 | rank = M.rank() 56 | logging.info(f"Extending initial matrix with rank {rank} (required = {l})...") 57 | while rank < l: 58 | Vi = vector(GF(e), l, sparse=True) 59 | factors = factor(hash_oracle(mi)) 60 | if all(p <= B for p, _ in factors): 61 | for p, v in factors: 62 | Vi[primes[p]] = v 63 | M_ = M.stack(Vi) 64 | rank_ = M_.rank() 65 | if rank_ > rank: 66 | M = M_ 67 | rank = rank_ 68 | m.append(mi) 69 | logging.debug(f"New rank: {rank}...") 70 | mi += 1 71 | 72 | logging.info(f"Found {M.nrows()}x{M.ncols()} basis matrix") 73 | 74 | b = M.solve_left(Vt) 75 | 76 | logging.info(f"Found linear combination of target vector") 77 | 78 | Vt = Vt.change_ring(ZZ) 79 | M = M.change_ring(ZZ) 80 | b = b.change_ring(ZZ) 81 | G = (Vt - b * M) / e 82 | delta = 1 83 | for pj, gj in zip(primes.keys(), G): 84 | delta = delta * pow(int(pj), int(gj), N) % N 85 | 86 | st = delta 87 | for bi, mi in zip(b, m): 88 | if bi > 0: 89 | si = sign_oracle(mi) 90 | st = st * pow(int(si), int(bi), N) % N 91 | 92 | return st 93 | -------------------------------------------------------------------------------- /attacks/rsa/extended_wiener_attack.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from sage.all import RR 5 | from sage.all import ZZ 6 | from sage.all import continued_fraction 7 | 8 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 9 | if sys.path[1] != path: 10 | sys.path.insert(1, path) 11 | 12 | from attacks.factorization import known_phi 13 | 14 | 15 | def attack(n, e, max_s=20000, max_r=100, max_t=100): 16 | """ 17 | Recovers the prime factors if the private exponent is too small. 18 | More information: Dujella A., "Continued fractions and RSA with small secret exponent" 19 | :param n: the modulus 20 | :param e: the public exponent 21 | :param max_s: the amount of s values to try (default: 20000) 22 | :param max_r: the amount of r values to try for each s value (default: 100) 23 | :param max_t: the amount of t values to try for each s value (default: 100) 24 | :return: a tuple containing the prime factors and the private exponent, or None if the private exponent was not found 25 | """ 26 | i_n = ZZ(n) 27 | i_e = ZZ(e) 28 | threshold = i_e / i_n + (RR(2.122) * i_e) / (i_n * i_n.sqrt()) 29 | convergents = continued_fraction(i_e / i_n).convergents() 30 | for i in range(1, len(convergents) - 2, 2): 31 | if convergents[i + 2] < threshold < convergents[i]: 32 | m = i 33 | break 34 | else: 35 | return None 36 | 37 | for s in range(max_s): 38 | for r in range(max_r): 39 | k = r * convergents[m + 1].numerator() + s * convergents[m + 1].numerator() 40 | d = r * convergents[m + 1].denominator() + s * convergents[m + 1].denominator() 41 | if pow(pow(2, e, n), d, n) != 2: 42 | continue 43 | 44 | phi = (e * d - 1) // k 45 | factors = known_phi.factorize(n, phi) 46 | if factors: 47 | return *factors, int(d) 48 | 49 | for t in range(max_t): 50 | k = s * convergents[m + 2].numerator() - t * convergents[m + 1].numerator() 51 | d = s * convergents[m + 2].denominator() - t * convergents[m + 1].denominator() 52 | if pow(pow(2, e, n), d, n) != 2: 53 | continue 54 | 55 | phi = (e * d - 1) // k 56 | factors = known_phi.factorize(n, phi) 57 | if factors: 58 | return *factors, int(d) 59 | -------------------------------------------------------------------------------- /attacks/rsa/hastad_attack.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from math import gcd 4 | 5 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 6 | if sys.path[1] != path: 7 | sys.path.insert(1, path) 8 | 9 | from attacks.rsa import low_exponent 10 | from shared.crt import fast_crt 11 | 12 | 13 | def attack(N, e, c): 14 | """ 15 | Recovers the plaintext from e ciphertexts, encrypted using different moduli and the same public exponent. 16 | :param N: the moduli 17 | :param e: the public exponent 18 | :param c: the ciphertexts 19 | :return: the plaintext 20 | """ 21 | assert e == len(N) == len(c), "The amount of ciphertexts should be equal to e." 22 | 23 | for i in range(len(N)): 24 | for j in range(len(N)): 25 | if i != j and gcd(N[i], N[j]) != 1: 26 | raise ValueError(f"Modulus {i} and {j} share factors, Hastad's attack is impossible.") 27 | 28 | c, _ = fast_crt(c, N) 29 | return low_exponent.attack(e, c) 30 | -------------------------------------------------------------------------------- /attacks/rsa/known_d.py: -------------------------------------------------------------------------------- 1 | from math import gcd 2 | from random import randrange 3 | 4 | 5 | def attack(N, e, d): 6 | """ 7 | Recovers the prime factors from a modulus if the public exponent and private exponent are known. 8 | :param N: the modulus 9 | :param e: the public exponent 10 | :param d: the private exponent 11 | :return: a tuple containing the prime factors 12 | """ 13 | k = e * d - 1 14 | t = 0 15 | while k % (2 ** t) == 0: 16 | t += 1 17 | 18 | while True: 19 | g = randrange(1, N) 20 | for s in range(1, t + 1): 21 | x = pow(g, k // (2 ** s), N) 22 | p = gcd(x - 1, N) 23 | if 1 < p < N and N % p == 0: 24 | return p, N // p 25 | -------------------------------------------------------------------------------- /attacks/rsa/low_exponent.py: -------------------------------------------------------------------------------- 1 | from sage.all import ZZ 2 | 3 | 4 | def attack(e, c): 5 | """ 6 | Recovers the plaintext from a ciphertext, encrypted using a very small public exponent (e.g. e = 3). 7 | :param e: the public exponent 8 | :param c: the ciphertext 9 | :return: the plaintext 10 | """ 11 | return int(ZZ(c).nth_root(e)) 12 | -------------------------------------------------------------------------------- /attacks/rsa/lsb_oracle.py: -------------------------------------------------------------------------------- 1 | from sage.all import ZZ 2 | 3 | 4 | def attack(N, e, c, oracle): 5 | """ 6 | Recovers the plaintext from the ciphertext using the LSB oracle (parity oracle) attack. 7 | :param N: the modulus 8 | :param e: the public exponent 9 | :param c: the encrypted message 10 | :param oracle: a function which returns the last bit of a plaintext for a given ciphertext 11 | :return: the plaintext 12 | """ 13 | left = ZZ(0) 14 | right = ZZ(N) 15 | while right - left > 1: 16 | c = (c * pow(2, e, N)) % N 17 | if oracle(c) == 0: 18 | right = (right + left) / 2 19 | else: 20 | left = (right + left) / 2 21 | 22 | return int(right) 23 | -------------------------------------------------------------------------------- /attacks/rsa/manger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 6 | if sys.path[1] != path: 7 | sys.path.insert(1, path) 8 | 9 | from shared import ceil_div 10 | from shared import floor_div 11 | 12 | 13 | # Step 1. 14 | def _step_1(padding_oracle, n, e, c): 15 | f1 = 2 16 | while padding_oracle((pow(f1, e, n) * c) % n): 17 | f1 *= 2 18 | 19 | return f1 20 | 21 | 22 | # Step 2. 23 | def _step_2(padding_oracle, n, e, c, B, f1): 24 | f2 = floor_div(n + B, B) * f1 // 2 25 | while not padding_oracle((pow(f2, e, n) * c) % n): 26 | f2 += f1 // 2 27 | 28 | return f2 29 | 30 | 31 | # Step 3. 32 | def _step_3(padding_oracle, n, e, c, B, f2): 33 | mmin = ceil_div(n, f2) 34 | mmax = floor_div(n + B, f2) 35 | while mmin < mmax: 36 | f = floor_div(2 * B, mmax - mmin) 37 | i = floor_div(f * mmin, n) 38 | f3 = ceil_div(i * n, mmin) 39 | if padding_oracle((pow(f3, e, n) * c) % n): 40 | mmax = floor_div(i * n + B, f3) 41 | else: 42 | mmin = ceil_div(i * n + B, f3) 43 | return mmin 44 | 45 | 46 | def attack(padding_oracle, n, e, c): 47 | """ 48 | Recovers the plaintext using Manger's attack. 49 | More information: Manger J., "A Chosen Ciphertext Attack on RSA Optimal Asymmetric Encryption Padding (OAEP) as Standardized in PKCS #1 v2.0" 50 | :param padding_oracle: the padding oracle taking integers, returns True if the PKCS #1 OAEP padding length is correct, False otherwise 51 | :param n: the modulus 52 | :param e: the public exponent 53 | :param c: the ciphertext (integer) 54 | :return: the plaintext (integer) 55 | """ 56 | k = ceil_div(n.bit_length(), 8) 57 | B = 2 ** (8 * (k - 1)) 58 | # TODO: extend at some point? 59 | assert 2 * B < n 60 | logging.info("Executing step 1...") 61 | f1 = _step_1(padding_oracle, n, e, c) 62 | logging.info("Executing step 2...") 63 | f2 = _step_2(padding_oracle, n, e, c, B, f1) 64 | logging.info("Executing step 3...") 65 | m = _step_3(padding_oracle, n, e, c, B, f2) 66 | return m 67 | -------------------------------------------------------------------------------- /attacks/rsa/nitaj_crt_rsa.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from math import gcd 5 | from math import log 6 | from math import sqrt 7 | 8 | from sage.all import RR 9 | from sage.all import Zmod 10 | 11 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 12 | if sys.path[1] != path: 13 | sys.path.insert(1, path) 14 | 15 | from shared.small_roots import herrmann_may_multivariate 16 | 17 | 18 | def attack(N, e, delta, m, t, check_bounds=True): 19 | """ 20 | Recovers the prime factors if one of the CRT-RSA private exponents is too small. 21 | More information: Nitaj A., "A new attack on RSA and CRT-RSA" (Section 4) 22 | :param N: the modulus 23 | :param e: the public exponent 24 | :param delta: the parameter delta such that dp <= N^delta 25 | :param m: the parameter m for small roots 26 | :param t: the parameter t for small roots 27 | :param check_bounds: perform bounds check (default: True) 28 | :return: a tuple containing the prime factors, or None if the factors could not be found 29 | """ 30 | alpha = log(e, N) 31 | assert not check_bounds or 2 * delta < sqrt(2) / 2 - alpha, f"Bounds check failed ({2 * delta} < {sqrt(2) / 2 - alpha})." 32 | 33 | x, y = Zmod(N)["x", "y"].gens() 34 | f = x + e * y 35 | X = int(RR(N) ** delta) 36 | Y = int(e * RR(N) ** (delta - 1 / 2)) # Equivalent to N^(alpha + delta - 1 / 2) 37 | logging.info(f"Trying {m = }, {t = }...") 38 | for x0, y0 in herrmann_may_multivariate.modular_multivariate(f, N, m, t, [X, Y]): 39 | pz = int(f(x0, y0)) 40 | p = gcd(pz, N) 41 | if 1 < p < N and N % p == 0: 42 | return p, N // p 43 | 44 | return None 45 | -------------------------------------------------------------------------------- /attacks/rsa/non_coprime_exponent.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from math import gcd 5 | 6 | from sage.all import GF 7 | from sage.all import crt 8 | from sage.all import is_prime 9 | 10 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 11 | if sys.path[1] != path: 12 | sys.path.insert(1, path) 13 | 14 | from attacks.factorization import known_phi 15 | from shared import rth_roots 16 | 17 | 18 | def attack(N, e, phi, c): 19 | """ 20 | Computes possible plaintexts when e is not coprime with Euler's totient. 21 | More information: Shumow D., "Incorrectly Generated RSA Keys: How To Recover Lost Plaintexts" 22 | :param N: the modulus 23 | :param e: the public exponent 24 | :param phi: Euler's totient for the modulus 25 | :param c: the ciphertext 26 | :return: a generator generating possible plaintexts for c 27 | """ 28 | assert phi % e == 0, "Public exponent must divide Euler's totient" 29 | if gcd(phi // e, e) == 1: 30 | assert is_prime(e), "Public exponent must be prime" 31 | phi //= e 32 | # Finding multiplicative generator of subgroup with order e elements (Algorithm 1). 33 | g = 1 34 | gE = 1 35 | while gE == 1: 36 | g += 1 37 | gE = pow(g, phi, N) 38 | 39 | # Finding possible plaintexts (Algorithm 2). 40 | d = pow(e, -1, phi) 41 | a = pow(c, d, N) 42 | l = gE 43 | for i in range(e): 44 | x = a * l % N 45 | l = l * gE % N 46 | yield x 47 | else: 48 | # Fall back to more generic root finding using Adleman-Manders-Miller and CRT. 49 | p, q = known_phi.factorize(N, phi) 50 | tp = 0 51 | while (p - 1) % (e ** (tp + 1)) == 0: 52 | tp += 1 53 | tq = 0 54 | while (q - 1) % (e ** (tq + 1)) == 0: 55 | tq += 1 56 | 57 | assert tp > 0 or tq > 0 58 | cp = c % p 59 | cq = c % q 60 | logging.info(f"Computing {e}-th roots mod {p}...") 61 | mps = [pow(cp, pow(e, -1, p - 1), p)] if tp == 0 else list(rth_roots(GF(p), cp, e)) 62 | logging.info(f"Computing {e}-th roots mod {q}...") 63 | mqs = [pow(cq, pow(e, -1, q - 1), q)] if tq == 0 else list(rth_roots(GF(q), cq, e)) 64 | logging.info(f"Computing {len(mps) * len(mqs)} roots using CRT...") 65 | for mp in mps: 66 | for mq in mqs: 67 | yield int(crt([mp, mq], [p, q])) 68 | -------------------------------------------------------------------------------- /attacks/rsa/related_message.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from itertools import product 5 | 6 | from sage.all import Zmod 7 | 8 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 9 | if sys.path[1] != path: 10 | sys.path.insert(1, path) 11 | 12 | from shared.polynomial import fast_polynomial_gcd 13 | 14 | 15 | def attack(N, e, c1, c2, f1, f2): 16 | """ 17 | Recovers the shared secret if p1 and p2 are affinely related and encrypted with the same modulus and exponent. 18 | Uses a fast GCD algorithm from "Polynomial Division and Greatest Common Divisors" 19 | :param N: the modulus 20 | :param e: the public exponent 21 | :param c1: the ciphertext of the first encryption 22 | :param c2: the ciphertext of the second encryption 23 | :param f1: the first function to apply to the shared secret 24 | :param f2: the second function to apply to the shared secret 25 | :return: the shared secret 26 | """ 27 | x = Zmod(N)["x"].gen() 28 | g1 = f1(x) ** e - c1 29 | g2 = f2(x) ** e - c2 30 | g = -fast_polynomial_gcd(g1, g2).monic() 31 | return int(g[0]) 32 | 33 | 34 | def attack_xor(N, e, c1, c2, x): 35 | """ 36 | Recovers the shared secret if p1 = p2 ^ x and encrypted with the same modulus and exponent. 37 | The complexity of this attack is 2^l, with l the hamming weight of x. 38 | :param N: the modulus 39 | :param e: the public exponent 40 | :param c1: the ciphertext of the first encryption 41 | :param c2: the ciphertext of the second encryption 42 | :param x: the XOR difference 43 | :return: a generator generating possible values of the shared secret 44 | """ 45 | shifts = [] 46 | for i in range(x.bit_length()): 47 | if (x >> i) & 1 == 1: 48 | shifts.append(1 << i) 49 | 50 | logging.info(f"Brute forcing 2^{len(shifts)} possibilities, this might take some time...") 51 | for signs in product([-1, 1], repeat=len(shifts)): 52 | difference = sum(sign * shift for sign, shift in zip(signs, shifts)) 53 | yield attack(N, e, c1, c2, lambda x: x, lambda x: x + difference) 54 | -------------------------------------------------------------------------------- /attacks/rsa/stereotyped_message.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | from sage.all import Zmod 6 | 7 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 8 | if sys.path[1] != path: 9 | sys.path.insert(1, path) 10 | 11 | from shared.small_roots import howgrave_graham 12 | 13 | 14 | def attack(N, e, c, partial_m, m=1, t=0): 15 | """ 16 | Recovers the plaintext from the ciphertext if some bits of the plaintext are known, using Coppersmith's method. 17 | :param N: the modulus 18 | :param e: the public exponent (should be "small": 3, 5, or 7 work best) 19 | :param c: the encrypted message 20 | :param partial_m: the partial plaintext message (PartialInteger) 21 | :param m: the m value to use for the small roots method (default: 1) 22 | :param t: the t value to use for the small roots method (default: 0) 23 | :return: the plaintext 24 | """ 25 | x = Zmod(N)["x"].gen() 26 | f = (partial_m.sub([x])) ** e - c 27 | X = partial_m.get_unknown_bounds() 28 | logging.info(f"Trying {m = }, {t = }...") 29 | for x0, in howgrave_graham.modular_univariate(f, N, m, t, X): 30 | if x0 != 0: 31 | return int(partial_m.sub([x0])) 32 | 33 | return None 34 | -------------------------------------------------------------------------------- /attacks/rsa/wiener_attack.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from sage.all import ZZ 5 | from sage.all import continued_fraction 6 | 7 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 8 | if sys.path[1] != path: 9 | sys.path.insert(1, path) 10 | 11 | from attacks.factorization import known_phi 12 | 13 | 14 | def attack(N, e): 15 | """ 16 | Recovers the prime factors of a modulus and the private exponent if the private exponent is too small. 17 | :param N: the modulus 18 | :param e: the public exponent 19 | :return: a tuple containing the prime factors and the private exponent, or None if the private exponent was not found 20 | """ 21 | convergents = continued_fraction(ZZ(e) / ZZ(N)).convergents() 22 | for c in convergents: 23 | k = c.numerator() 24 | d = c.denominator() 25 | if pow(pow(2, e, N), d, N) != 2: 26 | continue 27 | 28 | phi = (e * d - 1) // k 29 | factors = known_phi.factorize(N, phi) 30 | if factors: 31 | return *factors, int(d) 32 | -------------------------------------------------------------------------------- /attacks/rsa/wiener_attack_common_prime.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from math import log 5 | from math import sqrt 6 | 7 | from sage.all import RR 8 | from sage.all import ZZ 9 | 10 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 11 | if sys.path[1] != path: 12 | sys.path.insert(1, path) 13 | 14 | from shared.small_roots import jochemsz_may_integer 15 | 16 | 17 | def attack(N, e, delta=0.25, m=1, t=None, check_bounds=True): 18 | """ 19 | Recovers the prime factors of a modulus and the private exponent if the private exponent is too small (Common Prime RSA version). 20 | More information: Jochemsz E., May A., "A Strategy for Finding Roots of Multivariate Polynomials with New Applications in Attacking RSA Variants" (Section 5) 21 | :param N: the modulus 22 | :param e: the public exponent 23 | :param delta: a predicted bound on the private exponent (d < N^delta) (default: 0.25) 24 | :param m: the m value to use for the small roots method (default: 1) 25 | :param t: the t value to use for the small roots method (default: automatically computed using m) 26 | :param check_bounds: perform bounds check (default: True) 27 | :return: a tuple containing the prime factors and the private exponent, or None if the private exponent was not found 28 | """ 29 | gamma = 1 - log(e, N) 30 | assert not check_bounds or delta <= 1 / 4 * (4 + 4 * gamma - sqrt(13 + 20 * gamma + 4 * gamma ** 2)), f"Bounds check failed ({delta} <= {1 / 4 * (4 + 4 * gamma - sqrt(13 + 20 * gamma + 4 * gamma ** 2))})." 31 | 32 | x, y, z = ZZ["x", "y", "z"].gens() 33 | f = e ** 2 * x ** 2 + e * x * (y + z - 2) - (y + z - 1) - (N - 1) * y * z 34 | X = int(RR(N) ** delta) 35 | Y = int(RR(N) ** (delta - 1 / 2) * e) # Equivalent to N^(delta + 1 / 2 - gamma) 36 | Z = int(RR(N) ** (delta - 1 / 2) * e) # Equivalent to N^(delta + 1 / 2 - gamma) 37 | W = int(RR(N) ** (2 * delta) * e ** 2) # Equivalent to N^(2 * delta + 2 - 2 * gamma) 38 | t = int((1 / 2 + gamma - 4 * delta) / (2 * delta) * m) if t is None else t 39 | logging.info(f"Trying {m = }, {t = }...") 40 | strategy = jochemsz_may_integer.ExtendedStrategy([t, 0, 0]) 41 | for x0, y0, z0 in jochemsz_may_integer.integer_multivariate(f, m, W, [X, Y, Z], strategy): 42 | d = x0 43 | ka = y0 44 | kb = z0 45 | if pow(pow(2, e, N), d, N) == 2: 46 | p = (e * d - 1) // kb + 1 47 | q = (e * d - 1) // ka + 1 48 | return p, q, d 49 | 50 | return None 51 | -------------------------------------------------------------------------------- /attacks/rsa/wiener_attack_lattice.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from itertools import combinations 5 | from math import isqrt 6 | from math import prod 7 | 8 | from sage.all import RR 9 | from sage.all import ZZ 10 | from sage.all import matrix 11 | 12 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 13 | if sys.path[1] != path: 14 | sys.path.insert(1, path) 15 | 16 | from attacks.factorization import known_phi 17 | from shared.lattice import shortest_vectors 18 | from shared.small_roots import aono 19 | from shared.small_roots import reduce_lattice 20 | 21 | 22 | def attack(N, e): 23 | """ 24 | Recovers the prime factors of a modulus and the private exponent if the private exponent is too small. 25 | More information: Nguyen P. Q., "Public-Key Cryptanalysis" 26 | :param N: the modulus 27 | :param e: the public exponent 28 | :return: a tuple containing the prime factors and the private exponent, or None if the private exponent was not found 29 | """ 30 | s = isqrt(N) 31 | L = matrix(ZZ, [[e, s], [N, 0]]) 32 | 33 | for v in shortest_vectors(L): 34 | d = v[1] // s 35 | k = abs(v[0] - e * d) // N 36 | d = abs(d) 37 | if pow(pow(2, e, N), d, N) != 2: 38 | continue 39 | 40 | phi = (e * d - 1) // k 41 | factors = known_phi.factorize(N, phi) 42 | if factors: 43 | return *factors, int(d) 44 | 45 | return None 46 | 47 | 48 | # Construct R_{u, v} for a specific monomial. 49 | def _construct_relation(N, monomial, x): 50 | vars = monomial.variables() 51 | l = [x[i] for i in range(x.index(vars[-1]) + 1)] 52 | R = 1 53 | u = 0 54 | v = 0 55 | i = len(vars) 56 | for var in vars: 57 | if var != x[0] and var < l[0] and len(l) >= 2 * i: 58 | # Guo equation 59 | R *= l[0] - var 60 | l.pop(0) 61 | v += 1 62 | else: 63 | # Wiener equation 64 | R *= var - N 65 | u += 1 66 | 67 | l.remove(var) 68 | i -= 1 69 | 70 | return R, u, v 71 | 72 | 73 | def attack_multiple_exponents_1(N, e, alpha): 74 | """ 75 | Recovers the prime factors of a modulus given multiple public exponents with small corresponding private exponents. 76 | More information: Howgrave-Graham N., Seifert J., "Extending Wiener’s Attack in the Presence of Many Decrypting Exponents" 77 | :param N: the modulus 78 | :param e: the public exponent 79 | :param alpha: the bound on the private exponents (i.e. d < N^alpha) 80 | :return: a tuple containing the prime factors, or None if the prime factors were not found 81 | """ 82 | n = len(e) 83 | pr = ZZ[",".join(f"x{i}" for i in range(n))] 84 | x = pr.gens() 85 | 86 | monomials = [1] 87 | for i, xi in enumerate(x): 88 | monomials.append(xi) 89 | for j in range(i): 90 | for comb in combinations(x[:i], j + 1): 91 | monomials.append(prod(comb) * xi) 92 | 93 | L = matrix(ZZ, len(monomials)) 94 | exp_a = [n] 95 | exp_b = [0] 96 | for col, monomial in enumerate(monomials): 97 | if col == 0: 98 | L[0, 0] = 1 99 | continue 100 | 101 | R, u, v = _construct_relation(N, monomial, x) 102 | for row, monomial in enumerate(monomials): 103 | if row == 0: 104 | L[0, col] = R.constant_coefficient() 105 | else: 106 | L[row, col] = R.monomial_coefficient(monomial) * monomial(*e) 107 | 108 | exp_a.append(n - v) 109 | exp_b.append(u / 2) 110 | 111 | max_a = max(exp_a) 112 | max_b = max(exp_b) 113 | D = matrix(ZZ, len(monomials)) 114 | for i, (a, b) in enumerate(zip(exp_a, exp_b)): 115 | D[i, i] = int(RR(N) ** ((max_a - a) * alpha + (max_b - b))) 116 | 117 | L = L * D 118 | L_ = reduce_lattice(L) 119 | b = L.solve_left(L_[0]) 120 | phi = round(b[1] / b[0] * e[0]) 121 | return known_phi.factorize(N, phi) 122 | 123 | 124 | def attack_multiple_exponents_2(N, e, d_bit_length, m=1): 125 | """ 126 | Recovers the prime factors of a modulus given multiple public exponents with small corresponding private exponents. 127 | More information: Aono Y., "Minkowski sum based lattice construction for multivariate simultaneous Coppersmith’s technique and applications to RSA" (Section 4) 128 | :param N: the modulus 129 | :param e: the public exponent 130 | :param d_bit_length: the bit length of the private exponents 131 | :param m: the m value to use for the small roots method (default: 1) 132 | :return: a tuple containing the prime factors, or None if the prime factors were not found 133 | """ 134 | l = len(e) 135 | assert len(set(e)) == l, "All public exponents must be distinct" 136 | assert l >= 1, "At least one public exponent is required." 137 | 138 | pr = ZZ[",".join(f"x{i}" for i in range(l)) + ",y"] 139 | gens = pr.gens() 140 | x = gens[:-1] 141 | y = gens[-1] 142 | F = [-1 + x[k] * (y + N) for k in range(l)] 143 | X = [2 ** d_bit_length for _ in range(l)] 144 | Y = 3 * isqrt(N) 145 | logging.info(f"Trying {m = }...") 146 | for roots in aono.integer_multivariate(F, e, m, X + [Y]): 147 | phi = roots[y] + N 148 | factors = known_phi.factorize(N, phi) 149 | if factors: 150 | return factors 151 | 152 | return None 153 | -------------------------------------------------------------------------------- /attacks/shamir_secret_sharing/deterministic_coefficients.py: -------------------------------------------------------------------------------- 1 | def attack(p, k, a1, f, x, y): 2 | """ 3 | Recovers the shared secret if the coefficients are generated deterministically, and a single share is given. 4 | :param p: the prime used for Shamir's secret sharing 5 | :param k: the amount of shares needed to unlock the secret 6 | :param a1: the first coefficient of the polynomial 7 | :param f: a function which takes a coefficient and returns the next coefficient in the polynomial 8 | :param x: the x coordinate of the given share 9 | :param y: the y coordinate of the given share 10 | :return: the shared secret 11 | """ 12 | s = y 13 | a = a1 14 | for i in range(1, k): 15 | s -= a * x ** i 16 | a = f(a) 17 | 18 | return s % p 19 | -------------------------------------------------------------------------------- /attacks/shamir_secret_sharing/share_forgery.py: -------------------------------------------------------------------------------- 1 | def attack(p, s, s_, x, y, xs): 2 | """ 3 | Forges a share to recombine into a new shared secret, s', if a single share and the x coordinates of the other participants are given. 4 | :param p: the prime used for Shamir's secret sharing 5 | :param s: the original shared secret 6 | :param s_: the target shared secret, s' 7 | :param x: the x coordinate of the given share 8 | :param y: the y coordinate of the given share 9 | :param xs: the x coordinates of the other participants (excluding the x coordinate of the given share) 10 | :return: the forged share 11 | """ 12 | const = 1 13 | for i in xs: 14 | const *= i * pow(i - x, -1, p) 15 | 16 | return ((s_ - s) * pow(const, -1, p) + y) % p 17 | -------------------------------------------------------------------------------- /shared/complex_multiplication.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sage.all import EllipticCurve 4 | from sage.all import GF 5 | from sage.all import hilbert_class_polynomial 6 | from sage.all import is_prime_power 7 | 8 | 9 | def elementary_symmetric_function(x, k): 10 | assert k > 0 11 | 12 | if k > len(x): 13 | return 0 14 | 15 | e = [0] * k 16 | for i, xi in enumerate(x): 17 | ej = e[0] 18 | e[0] += xi 19 | ej_1 = ej 20 | for j in range(1, min(k, i + 1)): 21 | ej = e[j] 22 | e[j] += xi * ej_1 23 | ej_1 = ej 24 | 25 | return e[k - 1] 26 | 27 | 28 | def hilbert_class_polynomial_roots(D, gf): 29 | """ 30 | Computes the roots of H_D(X) mod q given D and GF(q). 31 | TODO: implement "Accelerating the CM method" by Sutherland. 32 | :param D: the CM discriminant (negative) 33 | :param gf: the finite field GF(q) 34 | :return: a generator generating the roots (values j) 35 | """ 36 | assert D < 0 and (D % 4 == 0 or D % 4 == 1), "D must be negative and a discriminant" 37 | H = hilbert_class_polynomial(D) 38 | pr = gf["x"] 39 | for j in pr(H).roots(multiplicities=False): 40 | yield j 41 | 42 | 43 | def generate_curve(gf, k, c=None): 44 | """ 45 | Generates an Elliptic Curve given GF(q), k, and parameter c 46 | :param gf: the finite field GF(q) 47 | :param k: j / (j - 1728) 48 | :param c: an optional parameter c which is used to generate random a and b values (default: random element in Zmod(q)) 49 | :return: 50 | """ 51 | c_ = c if c is not None else 0 52 | while c_ == 0: 53 | c_ = gf.random_element() 54 | 55 | a = 3 * k * c_ ** 2 56 | b = 2 * k * c_ ** 3 57 | return EllipticCurve(gf, [a, b]) 58 | 59 | 60 | def solve_cm(D, q, c=None): 61 | """ 62 | Solves a Complex Multiplication equation for a given discriminant D, prime q, and parameter c. 63 | :param D: the CM discriminant (negative) 64 | :param q: the prime q 65 | :param c: an optional parameter c which is used to generate random a and b values (default: random element in Zmod(q)) 66 | :return: a generator generating elliptic curves in Zmod(q) with random a and b values 67 | """ 68 | assert is_prime_power(q) 69 | 70 | logging.debug(f"Solving CM equation for {q = } using {D = } and {c = }") 71 | gf = GF(q) 72 | if gf.characteristic() == 2 or gf.characteristic() == 3: 73 | return 74 | 75 | ks = [] 76 | for j in hilbert_class_polynomial_roots(D, gf): 77 | if j != 0 and j != gf(1728): 78 | k = j / (1728 - j) 79 | yield generate_curve(gf, k, c) 80 | ks.append(k) 81 | 82 | while len(ks) > 0: 83 | for k in ks: 84 | yield generate_curve(gf, k, c) 85 | -------------------------------------------------------------------------------- /shared/crt.py: -------------------------------------------------------------------------------- 1 | from sage.all import crt 2 | from math import lcm 3 | 4 | 5 | def fast_crt(X, M, segment_size=8): 6 | """ 7 | Uses a divide-and-conquer algorithm to compute the CRT remainder and least common multiple. 8 | :param X: the remainders 9 | :param M: the moduli (not necessarily coprime) 10 | :param segment_size: the minimum size of the segments (default: 8) 11 | :return: a tuple containing the remainder and the least common multiple 12 | """ 13 | assert len(X) == len(M) 14 | assert len(X) > 0 15 | while len(X) > 1: 16 | X_ = [] 17 | M_ = [] 18 | for i in range(0, len(X), segment_size): 19 | if i == len(X) - 1: 20 | X_.append(X[i]) 21 | M_.append(M[i]) 22 | else: 23 | X_.append(crt(X[i:i + segment_size], M[i:i + segment_size])) 24 | M_.append(lcm(*M[i:i + segment_size])) 25 | X = X_ 26 | M = M_ 27 | 28 | return X[0], M[0] 29 | -------------------------------------------------------------------------------- /shared/hensel.py: -------------------------------------------------------------------------------- 1 | from sage.all import ZZ 2 | from sage.all import Zmod 3 | 4 | 5 | def hensel_lift_linear(f, p, k, roots): 6 | """ 7 | Uses Hensel lifting to lift the roots of f mod p^k to f mod p^(k + 1) 8 | :param f: the polynomial 9 | :param p: the prime 10 | :param k: the power 11 | :param roots: a generator generating the roots of f mod p^k 12 | :return: a generator generating the roots of f mod p^(k + 1) 13 | """ 14 | pk = p ** k 15 | pk1 = p ** (k + 1) 16 | for root in roots: 17 | # We're really not using Hensel's lemma correctly here... 18 | # Maybe this will be fixed later 19 | for i in range(p): 20 | new_root = root + i * pk 21 | if f(new_root) % pk1 == 0: 22 | yield new_root 23 | 24 | 25 | def hensel_roots(f, p, k): 26 | """ 27 | Uses Hensel lifting to generate the roots of f mod p^k. 28 | :param f: the polynomial 29 | :param p: the prime 30 | :param k: the power 31 | :return: a generator generating the roots of f mod p^k 32 | """ 33 | f_ = f.change_ring(Zmod(p)) 34 | if f_ == 0: 35 | roots = range(p) 36 | elif f_.is_constant(): 37 | return 38 | else: 39 | roots = map(int, f_.roots(multiplicities=False)) 40 | 41 | f = f.change_ring(ZZ) 42 | for i in range(1, k): 43 | roots = hensel_lift_linear(f, p, i, roots) 44 | 45 | return roots 46 | -------------------------------------------------------------------------------- /shared/lattice.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def shortest_vectors(B): 5 | """ 6 | Computes the shortest non-zero vectors in a lattice. 7 | :param B: the basis of the lattice 8 | :return: a generator generating the shortest non-zero vectors 9 | """ 10 | logging.debug(f"Computing shortest vectors in {B.nrows()} x {B.ncols()} matrix...") 11 | B = B.LLL() 12 | 13 | for row in B.rows(): 14 | if not row.is_zero(): 15 | yield row 16 | 17 | 18 | # Babai's Nearest Plane Algorithm from "Lecture 3: CVP Algorithm" by Oded Regev. 19 | def _closest_vectors_babai(B, t): 20 | B = B.LLL() 21 | 22 | for G in B.gram_schmidt(): 23 | b = t 24 | for j in reversed(range(B.nrows())): 25 | b -= round((b * G[j]) / (G[j] * G[j])) * B[j] 26 | 27 | yield t - b 28 | 29 | 30 | def _closest_vectors_embedding(B, t): 31 | B_ = B.new_matrix(B.nrows() + 1, B.ncols() + 1) 32 | for row in range(B.nrows()): 33 | for col in range(B.ncols()): 34 | B_[row, col] = B[row, col] 35 | 36 | for col in range(B.ncols()): 37 | B_[B.nrows(), col] = t[col] 38 | 39 | B_[B.nrows(), B.ncols()] = 1 40 | yield from shortest_vectors(B_) 41 | 42 | 43 | def closest_vectors(B, t, algorithm="embedding"): 44 | """ 45 | Computes the closest vectors in a lattice to a target vector. 46 | :param B: the basis of the lattice 47 | :param t: the target vector 48 | :param algorithm: the algorithm to use, can be "babai" or "embedding" (default: "embedding") 49 | :return: a generator generating the shortest non-zero vectors 50 | """ 51 | logging.debug(f"Computing closest vectors in {B.nrows()} x {B.ncols()} matrix...") 52 | if algorithm == "babai": 53 | yield from _closest_vectors_babai(B, t) 54 | elif algorithm == "embedding": 55 | yield from _closest_vectors_embedding(B, t) 56 | -------------------------------------------------------------------------------- /shared/matrices.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sage.all import GF 4 | from sage.all import identity_matrix 5 | from sage.matrix.matrix2 import _jordan_form_vector_in_difference 6 | 7 | 8 | def find_eigenvalues(A): 9 | """ 10 | Computes the eigenvalues and P matrices for a specific matrix A. 11 | :param A: the matrix A. 12 | :return: a generator generating tuples of 13 | K: the extension field of the eigenvalue, 14 | k: the degree of the factor of the charpoly associated with the eigenvalue, 15 | e: the multiplicity of the factor of the charpoly associated with the eigenvalue, 16 | l: the eigenvalue, 17 | P: the transformation matrix P (only the first e columns are filled) 18 | """ 19 | factors = {} 20 | for g, e in A.charpoly().factor(): 21 | k = g.degree() 22 | if k not in factors or e > factors[k][0]: 23 | factors[k] = (e, g) 24 | 25 | p = A.base_ring().order() 26 | for k, (e, g) in factors.items(): 27 | logging.debug(f"Found factor {g} with degree {k} and multiplicity {e}") 28 | K = GF(p ** k, "x", modulus=g, impl="modn" if k == 1 else "pari") 29 | l = K.gen() 30 | # Assuming there is only 1 Jordan block for this eigenvalue. 31 | Vlarge = ((A - l) ** e).right_kernel().basis() 32 | Vsmall = ((A - l) ** (e - 1)).right_kernel().basis() 33 | v = _jordan_form_vector_in_difference(Vlarge, Vsmall) 34 | P = identity_matrix(K, A.nrows()) 35 | for i in reversed(range(e)): 36 | P.set_row(i, v) 37 | v = (A - l) * v 38 | 39 | P = P.transpose() 40 | yield K, k, e, l, P 41 | 42 | 43 | def dlog(A, B): 44 | """ 45 | Computes l such that A^l = B. 46 | :param A: the matrix A 47 | :param B: the matrix B 48 | :return: a generator generating values for l and m, where A^l = B mod m. 49 | """ 50 | assert A.is_square() and B.is_square() and A.nrows() == B.nrows() 51 | 52 | p = A.base_ring().order() 53 | for K, k, e, l, P in find_eigenvalues(A): 54 | B_ = P ** -1 * B * P 55 | logging.debug(f"Computing dlog in {K}...") 56 | yield int(B_[0, 0].log(l)), int(p ** k - 1) 57 | if e >= 2: 58 | B1 = B_[e - 1, e - 1] 59 | B2 = B_[e - 2, e - 1] 60 | yield int((l * B2) / B1), int(p ** k) 61 | 62 | 63 | def dlog_equation(A, x, y): 64 | """ 65 | Computes l such that A^l * x = y, in GF(p). 66 | :param A: the matrix A 67 | :param x: the vector x 68 | :param y: the vector y 69 | :return: l, or None if l could not be found 70 | """ 71 | assert A.is_square() 72 | 73 | # TODO: extend to GF(p^k) if necessary? 74 | J, P = A.jordan_form(transformation=True) 75 | x = P ** -1 * x 76 | y = P ** -1 * y 77 | r = 0 78 | for s1, s2 in zip(*J.subdivisions()): 79 | S = J.subdivision(s1, s2) 80 | assert S.is_square() 81 | 82 | n = S.nrows() 83 | r += n 84 | if n >= 2: 85 | x1 = x[r - 1] 86 | x2 = x[r] 87 | y1 = y[r - 1] 88 | y2 = y[r] 89 | l = S[n - 1, n - 1] * (y1 - x1 * y2 / x2) / y2 90 | return int(l) 91 | 92 | return None 93 | -------------------------------------------------------------------------------- /shared/polynomial.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sage.all import ZZ 4 | from sage.all import Zmod 5 | 6 | from shared.crt import fast_crt 7 | 8 | 9 | def _polynomial_hgcd(ring, a0, a1): 10 | assert a1.degree() < a0.degree() 11 | 12 | if a1.degree() <= a0.degree() / 2: 13 | return 1, 0, 0, 1 14 | 15 | m = a0.degree() // 2 16 | b0 = ring(a0.list()[m:]) 17 | b1 = ring(a1.list()[m:]) 18 | R00, R01, R10, R11 = _polynomial_hgcd(ring, b0, b1) 19 | d = R00 * a0 + R01 * a1 20 | e = R10 * a0 + R11 * a1 21 | if e.degree() < m: 22 | return R00, R01, R10, R11 23 | 24 | q, f = d.quo_rem(e) 25 | g0 = ring(e.list()[m // 2:]) 26 | g1 = ring(f.list()[m // 2:]) 27 | S00, S01, S10, S11 = _polynomial_hgcd(ring, g0, g1) 28 | return S01 * R00 + (S00 - q * S01) * R10, S01 * R01 + (S00 - q * S01) * R11, S11 * R00 + (S10 - q * S11) * R10, S11 * R01 + (S10 - q * S11) * R11 29 | 30 | 31 | def fast_polynomial_gcd(a0, a1): 32 | """ 33 | Uses a divide-and-conquer algorithm (HGCD) to compute the polynomial gcd. 34 | More information: Aho A. et al., "The Design and Analysis of Computer Algorithms" (Section 8.9) 35 | :param a0: the first polynomial 36 | :param a1: the second polynomial 37 | :return: the polynomial gcd 38 | """ 39 | # TODO: implement extended variant of half GCD? 40 | assert a0.parent() == a1.parent() 41 | 42 | if a0.degree() == a1.degree(): 43 | if a1 == 0: 44 | return a0 45 | a0, a1 = a1, a0 % a1 46 | elif a0.degree() < a1.degree(): 47 | a0, a1 = a1, a0 48 | 49 | assert a0.degree() > a1.degree() 50 | ring = a0.parent() 51 | 52 | # Optimize recursive tail call. 53 | while True: 54 | logging.debug(f"deg(a0) = {a0.degree()}, deg(a1) = {a1.degree()}") 55 | _, r = a0.quo_rem(a1) 56 | if r == 0: 57 | return a1.monic() 58 | 59 | R00, R01, R10, R11 = _polynomial_hgcd(ring, a0, a1) 60 | b0 = R00 * a0 + R01 * a1 61 | b1 = R10 * a0 + R11 * a1 62 | if b1 == 0: 63 | return b0.monic() 64 | 65 | _, r = b0.quo_rem(b1) 66 | if r == 0: 67 | return b1.monic() 68 | 69 | a0 = b1 70 | a1 = r 71 | 72 | 73 | def polynomial_gcd_crt(a, b, factors): 74 | """ 75 | Uses the Chinese Remainder Theorem to compute the polynomial gcd modulo a composite number. 76 | :param a: the first polynomial 77 | :param b: the second polynomial 78 | :param factors: the factors of m (tuples of primes and exponents) 79 | :return: the polynomial gcd modulo m 80 | """ 81 | assert a.base_ring() == b.base_ring() == ZZ 82 | 83 | gs = [] 84 | ps = [] 85 | for p, _ in factors: 86 | zmodp = Zmod(p) 87 | gs.append(fast_polynomial_gcd(a.change_ring(zmodp), b.change_ring(zmodp)).change_ring(ZZ)) 88 | ps.append(p) 89 | 90 | g, _ = fast_crt(gs, ps) 91 | return g 92 | 93 | 94 | def polynomial_xgcd(a, b): 95 | """ 96 | Computes the extended GCD of two polynomials using Euclid's algorithm. 97 | :param a: the first polynomial 98 | :param b: the second polynomial 99 | :return: a tuple containing r, s, and t 100 | """ 101 | assert a.base_ring() == b.base_ring() 102 | 103 | r_prev, r = a, b 104 | s_prev, s = 1, 0 105 | t_prev, t = 0, 1 106 | 107 | while r: 108 | try: 109 | q = r_prev // r 110 | r_prev, r = r, r_prev - q * r 111 | s_prev, s = s, s_prev - q * s 112 | t_prev, t = t, t_prev - q * t 113 | except RuntimeError: 114 | raise ArithmeticError("r is not invertible", r) 115 | 116 | return r_prev, s_prev, t_prev 117 | 118 | 119 | def polynomial_inverse(p, m): 120 | """ 121 | Computes the inverse of a polynomial modulo a polynomial using the extended GCD. 122 | :param p: the polynomial 123 | :param m: the polynomial modulus 124 | :return: the inverse of p modulo m 125 | """ 126 | g, s, t = polynomial_xgcd(p, m) 127 | return s * g.lc() ** -1 128 | 129 | 130 | def max_norm(p): 131 | """ 132 | Computes the max norm (infinity norm) of a polynomial. 133 | :param p: the polynomial 134 | :return: a tuple containing the monomial degrees of the largest coefficient and the coefficient 135 | """ 136 | max_degs = None 137 | max_coeff = 0 138 | for degs, coeff in p.dict().items(): 139 | if abs(coeff) > max_coeff: 140 | max_degs = degs 141 | max_coeff = abs(coeff) 142 | 143 | return max_degs, max_coeff 144 | -------------------------------------------------------------------------------- /shared/small_roots/aono.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from itertools import product 3 | from math import prod 4 | 5 | from sage.all import ZZ 6 | from sage.all import xgcd 7 | 8 | from shared import small_roots 9 | 10 | 11 | def integer_multivariate(F, e, m, X, roots_method="groebner"): 12 | """ 13 | Computes small integer roots of a list of polynomials. 14 | More information: Aono Y., "Minkowski sum based lattice construction for multivariate simultaneous Coppersmith's technique and applications to RSA" (Section 4) 15 | :param F: the list of polynomials 16 | :param e: the list of e values 17 | :param m: the parameter m 18 | :param X: an approximate bound on the x roots 19 | :param roots_method: the method to use to find roots (default: "groebner") 20 | :return: a generator generating small roots (dicts of (x0: x0root, x1: x1root, ..., y: yroot) entries) of the polynomials 21 | """ 22 | # We need lexicographic ordering for .lc() below. 23 | pr = F[0].parent().change_ring(ZZ, order="lex") 24 | x = pr.gens() 25 | 26 | l = len(e) 27 | for k in range(l): 28 | F[k] = pr(F[k]) 29 | 30 | logging.debug("Generating shifts...") 31 | 32 | g = [] 33 | for k in range(l): 34 | gk = {} 35 | for i in range(m + 1): 36 | for j in range(i + 1): 37 | gk[i, j] = x[k] ** (i - j) * F[k] ** j * e[k] ** (m - j) 38 | g.append(gk) 39 | 40 | Ig = {} 41 | for tup in product(*g): 42 | g_ = prod(g[k][tup[k]] for k in range(l)) 43 | index = tuple(g_.exponents()[0]) 44 | if index not in Ig: 45 | Ig[index] = [] 46 | Ig[index].append(g_) 47 | 48 | shifts = [] 49 | for g in Ig.values(): 50 | gp = g[0] 51 | lc = gp.lc() 52 | for gi in g[1:]: 53 | lc, s, t = xgcd(lc, gi.lc()) 54 | gp = s * gp + t * gi 55 | shifts.append(gp) 56 | 57 | L, monomials = small_roots.create_lattice(pr, shifts, X) 58 | L = small_roots.reduce_lattice(L) 59 | polynomials = small_roots.reconstruct_polynomials(L, None, prod(e) ** m, monomials, X) 60 | yield from small_roots.find_roots(pr, polynomials, method=roots_method) 61 | -------------------------------------------------------------------------------- /shared/small_roots/blomer_may.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sage.all import ZZ 4 | 5 | from shared import small_roots 6 | 7 | 8 | def modular_trivariate(f, N, m, t, X, Y, Z, roots_method="groebner"): 9 | """ 10 | Computes small modular roots of a trivariate polynomial. 11 | More information: Blomer J., May A., "New Partial Key Exposure Attacks on RSA" (Section 4) 12 | :param f: the polynomial 13 | :param N: the modulus 14 | :param m: the parameter m 15 | :param t: the parameter t 16 | :param X: an approximate bound on the x roots 17 | :param Y: an approximate bound on the y roots 18 | :param Z: an approximate bound on the z roots 19 | :param roots_method: the method to use to find roots (default: "groebner") 20 | :return: a generator generating small roots (tuples of x, y, and z roots) of the polynomial 21 | """ 22 | f = f.change_ring(ZZ) 23 | pr = f.parent() 24 | x, y, z = pr.gens() 25 | 26 | logging.debug("Generating shifts...") 27 | 28 | shifts = [] 29 | for i in range(m + 1): 30 | for j in range(i + 1): 31 | for k in range(j + 1): 32 | g = x ** (j - k) * z ** k * N ** i * f ** (m - i) 33 | shifts.append(g) 34 | 35 | for k in range(1, t + 1): 36 | h = x ** j * y ** k * N ** i * f ** (m - i) 37 | shifts.append(h) 38 | 39 | L, monomials = small_roots.create_lattice(pr, shifts, [X, Y, Z]) 40 | L = small_roots.reduce_lattice(L) 41 | polynomials = small_roots.reconstruct_polynomials(L, f, N ** m, monomials, [X, Y, Z]) 42 | for roots in small_roots.find_roots(pr, polynomials, method=roots_method): 43 | yield roots[x], roots[y], roots[z] 44 | 45 | 46 | def modular_bivariate(f, eM, m, t, Y, Z, roots_method="groebner"): 47 | """ 48 | Computes small modular roots of a bivariate polynomial. 49 | More information: Blomer J., May A., "New Partial Key Exposure Attacks on RSA" (Section 6) 50 | :param f: the polynomial 51 | :param eM: the modulus 52 | :param m: the parameter m 53 | :param t: the parameter t 54 | :param Y: an approximate bound on the y roots 55 | :param Z: an approximate bound on the z roots 56 | :param roots_method: the method to use to find roots (default: "groebner") 57 | :return: a generator generating small roots (tuples of y and z roots) of the polynomial 58 | """ 59 | f = f.change_ring(ZZ) 60 | pr = f.parent() 61 | y, z = pr.gens() 62 | 63 | logging.debug("Generating shifts...") 64 | 65 | shifts = [] 66 | for i in range(m + 1): 67 | for j in range(i + 1): 68 | g = y ** j * eM ** i * f ** (m - i) 69 | shifts.append(g) 70 | 71 | for j in range(1, t + 1): 72 | h = z ** j * eM ** i * f ** (m - i) 73 | shifts.append(h) 74 | 75 | L, monomials = small_roots.create_lattice(pr, shifts, [Y, Z]) 76 | L = small_roots.reduce_lattice(L) 77 | polynomials = small_roots.reconstruct_polynomials(L, f, eM ** m, monomials, [Y, Z]) 78 | for roots in small_roots.find_roots(pr, polynomials, method=roots_method): 79 | yield roots[y], roots[z] 80 | -------------------------------------------------------------------------------- /shared/small_roots/boneh_durfee.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sage.all import ZZ 4 | 5 | from shared import small_roots 6 | 7 | 8 | def modular_bivariate(f, e, m, t, X, Y, roots_method="groebner"): 9 | """ 10 | Computes small modular roots of a bivariate polynomial. 11 | More information: Boneh D., Durfee G., "Cryptanalysis of RSA with Private Key d Less than N^0.292" 12 | :param f: the polynomial 13 | :param e: the modulus 14 | :param m: the amount of normal shifts to use 15 | :param t: the amount of additional shifts to use 16 | :param X: an approximate bound on the x roots 17 | :param Y: an approximate bound on the y roots 18 | :param roots_method: the method to use to find roots (default: "groebner") 19 | :return: a generator generating small roots (tuples of x and y roots) of the polynomial 20 | """ 21 | f = f.change_ring(ZZ) 22 | pr = f.parent() 23 | x, y = pr.gens() 24 | 25 | logging.debug("Generating shifts...") 26 | 27 | shifts = [] 28 | for k in range(m + 1): 29 | for i in range(m - k + 1): 30 | g = x ** i * f ** k * e ** (m - k) 31 | shifts.append(g) 32 | 33 | for j in range(t + 1): 34 | h = y ** j * f ** k * e ** (m - k) 35 | shifts.append(h) 36 | 37 | L, monomials = small_roots.create_lattice(pr, shifts, [X, Y]) 38 | L = small_roots.reduce_lattice(L) 39 | polynomials = small_roots.reconstruct_polynomials(L, f, e ** m, monomials, [X, Y]) 40 | for roots in small_roots.find_roots(pr, polynomials, method=roots_method): 41 | yield roots[x], roots[y] 42 | -------------------------------------------------------------------------------- /shared/small_roots/coron.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from math import gcd 3 | 4 | from sage.all import ZZ 5 | 6 | from shared import small_roots 7 | from shared.polynomial import max_norm 8 | 9 | 10 | def integer_bivariate(p, k, X, Y, roots_method="groebner"): 11 | """ 12 | Computes small integer roots of a bivariate polynomial. 13 | More information: Coron J., "Finding Small Roots of Bivariate Integer Polynomial Equations Revisited" 14 | Note: integer_bivariate in the coron_direct will probably be more efficient. 15 | :param p: the polynomial 16 | :param k: the amount of shifts to use 17 | :param X: an approximate bound on the x roots 18 | :param Y: an approximate bound on the y roots 19 | :param roots_method: the method to use to find roots (default: "groebner") 20 | :return: a generator generating small roots (tuples of x and y roots) of the polynomial 21 | """ 22 | pr = p.parent() 23 | x, y = pr.gens() 24 | delta = max(p.degrees()) 25 | 26 | _, W = max_norm(p(x * X, y * Y)) 27 | 28 | p00 = int(p.constant_coefficient()) 29 | assert p00 != 0 30 | while gcd(p00, X) != 1: 31 | X += 1 32 | while gcd(p00, Y) != 1: 33 | Y += 1 34 | while gcd(p00, W) != 1: 35 | W += 1 36 | 37 | u = W + (1 - W) % abs(p00) 38 | n = u * (X * Y) ** k 39 | assert gcd(p00, n) == 1 40 | q = ((pow(p00, -1, n) * p) % n).change_ring(ZZ) 41 | 42 | logging.debug("Generating shifts...") 43 | 44 | shifts = [] 45 | for i in range(k + delta + 1): 46 | for j in range(k + delta + 1): 47 | if i <= k and j <= k: 48 | shifts.append(x ** i * y ** j * X ** (k - i) * Y ** (k - j) * q) 49 | else: 50 | shifts.append(x ** i * y ** j * n) 51 | 52 | L, monomials = small_roots.create_lattice(pr, shifts, [X, Y]) 53 | L = small_roots.reduce_lattice(L) 54 | polynomials = small_roots.reconstruct_polynomials(L, p, n, monomials, [X, Y]) 55 | for roots in small_roots.find_roots(pr, [p] + polynomials, method=roots_method): 56 | yield roots[x], roots[y] 57 | -------------------------------------------------------------------------------- /shared/small_roots/coron_direct.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sage.all import ZZ 4 | from sage.all import matrix 5 | 6 | from shared import small_roots 7 | from shared.polynomial import max_norm 8 | 9 | 10 | def integer_bivariate(p, k, X, Y, echelon_algorithm="default", roots_method="groebner"): 11 | """ 12 | Computes small integer roots of a bivariate polynomial. 13 | More information: Coron J., "Finding Small Roots of Bivariate Integer Polynomial Equations: a Direct Approach" 14 | :param p: the polynomial 15 | :param k: the amount of shifts to use 16 | :param X: an approximate bound on the x roots 17 | :param Y: an approximate bound on the y roots 18 | :param echelon_algorithm: the algorithm to use to calculate the Echelon form of L (default: "default") 19 | :param roots_method: the method to use to find roots (default: "groebner") 20 | :return: a generator generating small roots (tuples of x and y roots) of the polynomial 21 | """ 22 | pr = p.parent() 23 | x, y = pr.gens() 24 | delta = max(p.degrees()) 25 | 26 | (i0, j0), W = max_norm(p(x * X, y * Y)) 27 | 28 | logging.debug("Calculating n...") 29 | S = matrix(ZZ, k ** 2, k ** 2) 30 | for a in range(k): 31 | for b in range(k): 32 | s = x ** a * y ** b * p 33 | for i in range(k): 34 | for j in range(k): 35 | S[a * k + b, i * k + j] = s.coefficient([i0 + i, j0 + j]) 36 | 37 | n = abs(S.det()) 38 | logging.debug(f"Found {n = }") 39 | 40 | # Monomials are collected in "left" and "right" lists, which determine where the columns are in relation to each other. 41 | # This partition ensures the Echelon form will set desired monomial coefficients to zero. 42 | logging.debug("Generating monomials...") 43 | left_monomials = [] 44 | right_monomials = [] 45 | for i in range(k + delta): 46 | for j in range(k + delta): 47 | if 0 <= i - i0 < k and 0 <= j - j0 < k: 48 | left_monomials.append(x ** i * y ** j) 49 | else: 50 | right_monomials.append(x ** i * y ** j) 51 | 52 | assert len(left_monomials) == k ** 2 53 | monomials = left_monomials + right_monomials 54 | 55 | logging.debug("Generating shifts...") 56 | 57 | shifts = [] 58 | for a in range(k): 59 | for b in range(k): 60 | s = x ** a * y ** b * p 61 | shifts.append(s) 62 | 63 | for monomial in monomials: 64 | r = monomial * n 65 | shifts.append(r) 66 | 67 | logging.debug(f"Filling the lattice ({len(shifts)} x {len(monomials)})...") 68 | L = matrix(ZZ, len(shifts), len(monomials)) 69 | for row, shift in enumerate(shifts): 70 | for col, monomial in enumerate(monomials): 71 | L[row, col] = shift.monomial_coefficient(monomial) * monomial(X, Y) 72 | 73 | logging.debug("Generating Echelon form...") 74 | L = L.echelon_form(algorithm=echelon_algorithm) 75 | 76 | L2 = L.submatrix(k ** 2, k ** 2, (k + delta) ** 2 - k ** 2) 77 | L2 = small_roots.reduce_lattice(L2) 78 | # Only use right monomials now (corresponding the the sublattice). 79 | polynomials = small_roots.reconstruct_polynomials(L2, p, n, right_monomials, [X, Y]) 80 | for roots in small_roots.find_roots(pr, [p] + polynomials, method=roots_method): 81 | yield roots[x], roots[y] 82 | -------------------------------------------------------------------------------- /shared/small_roots/ernst.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sage.all import RR 4 | from sage.all import ZZ 5 | from sage.all import gcd 6 | 7 | from shared import small_roots 8 | 9 | 10 | def integer_trivariate_1(f, m, t, W, X, Y, Z, check_bounds=True, roots_method="groebner"): 11 | """ 12 | Computes small integer roots of a trivariate polynomial. 13 | More information: Ernst M. et al., "Partial Key Exposure Attacks on RSA Up to Full Size Exponents" (Section 4.1.1) 14 | :param f: the polynomial 15 | :param m: the parameter m 16 | :param t: the parameter t 17 | :param W: the parameter W 18 | :param X: an approximate bound on the x roots 19 | :param Y: an approximate bound on the y roots 20 | :param Z: an approximate bound on the z roots 21 | :param check_bounds: perform bounds check (default: True) 22 | :param roots_method: the method to use to find roots (default: "groebner") 23 | :return: a generator generating small roots (tuples of x, y, and z roots) of the polynomial 24 | """ 25 | pr = f.parent() 26 | x, y, z = pr.gens() 27 | 28 | tau = t / m 29 | if check_bounds and RR(X) ** (1 + 3 * tau) * RR(Y) ** (2 + 3 * tau) * RR(Z) ** (1 + 3 * tau + 3 * tau ** 2) > RR(W) ** (1 + 3 * tau): 30 | logging.debug(f"Bound check failed for {m = }, {t = }") 31 | return 32 | 33 | R = int(f.constant_coefficient()) 34 | assert R != 0 35 | while gcd(R, X) != 1: 36 | X += 1 37 | while gcd(R, Y) != 1: 38 | Y += 1 39 | while gcd(R, Z) != 1: 40 | Z += 1 41 | while gcd(R, W) != 1: 42 | W += 1 43 | 44 | n = (X * Y) ** m * Z ** (m + t) * W 45 | assert gcd(R, n) == 1 46 | f_ = (pow(R, -1, n) * f % n).change_ring(ZZ) 47 | 48 | logging.debug("Generating shifts...") 49 | 50 | shifts = [] 51 | for i in range(m + 1): 52 | for j in range(m - i + 1): 53 | for k in range(j + 1): 54 | g = x ** i * y ** j * z ** k * f_ * X ** (m - i) * Y ** (m - j) * Z ** (m + t - k) 55 | shifts.append(g) 56 | 57 | for k in range(j + 1, j + t + 1): 58 | h = x ** i * y ** j * z ** k * f_ * X ** (m - i) * Y ** (m - j) * Z ** (m + t - k) 59 | shifts.append(h) 60 | 61 | for i in range(m + 2): 62 | j = m + 1 - i 63 | for k in range(j + 1): 64 | g_ = n * x ** i * y ** j * z ** k 65 | shifts.append(g_) 66 | 67 | for k in range(j + 1, j + t + 1): 68 | h_ = n * x ** i * y ** j * z ** k 69 | shifts.append(h_) 70 | 71 | L, monomials = small_roots.create_lattice(pr, shifts, [X, Y, Z]) 72 | L = small_roots.reduce_lattice(L) 73 | polynomials = small_roots.reconstruct_polynomials(L, f, n, monomials, [X, Y, Z]) 74 | for roots in small_roots.find_roots(pr, [f] + polynomials, method=roots_method): 75 | yield roots[x], roots[y], roots[z] 76 | 77 | 78 | def integer_trivariate_2(f, m, t, W, X, Y, Z, check_bounds=True, roots_method="groebner"): 79 | """ 80 | Computes small integer roots of a trivariate polynomial. 81 | More information: Ernst M. et al., "Partial Key Exposure Attacks on RSA Up to Full Size Exponents" (Section 4.1.2) 82 | :param f: the polynomial 83 | :param m: the parameter m 84 | :param t: the parameter t 85 | :param W: the parameter W 86 | :param X: an approximate bound on the x roots 87 | :param Y: an approximate bound on the y roots 88 | :param Z: an approximate bound on the z roots 89 | :param check_bounds: perform bounds check (default: True) 90 | :param roots_method: the method to use to find roots (default: "groebner") 91 | :return: a generator generating small roots (tuples of x, y, and z roots) of the polynomial 92 | """ 93 | pr = f.parent() 94 | x, y, z = pr.gens() 95 | 96 | tau = t / m 97 | if check_bounds and RR(X) ** (2 + 3 * tau) * RR(Y) ** (3 + 6 * tau + 3 * tau ** 2) * RR(Z) ** (3 + 3 * tau) > RR(W) ** (2 + 3 * tau): 98 | logging.debug(f"Bound check failed for {m = }, {t = }") 99 | return 100 | 101 | R = int(f.constant_coefficient()) 102 | assert R != 0 103 | while gcd(R, X) != 1: 104 | X += 1 105 | while gcd(R, Y) != 1: 106 | Y += 1 107 | while gcd(R, Z) != 1: 108 | Z += 1 109 | while gcd(R, W) != 1: 110 | W += 1 111 | 112 | n = X ** m * Y ** (m + t) * Z ** m * W 113 | assert gcd(R, n) == 1 114 | f_ = (pow(R, -1, n) * f % n).change_ring(ZZ) 115 | 116 | logging.debug("Generating shifts...") 117 | 118 | shifts = [] 119 | for i in range(m + 1): 120 | for j in range(m - i + 1): 121 | for k in range(m - i + 1): 122 | g = x ** i * y ** j * z ** k * f_ * X ** (m - i) * Y ** (m + t - j) * Z ** (m - k) 123 | shifts.append(g) 124 | 125 | for j in range(m - i + 1, m - i + t + 1): 126 | for k in range(m - i + 1): 127 | h = x ** i * y ** j * z ** k * f_ * X ** (m - i) * Y ** (m + t - j) * Z ** (m - k) 128 | shifts.append(h) 129 | 130 | for i in range(m + 2): 131 | for j in range(m + t + 2 - i): 132 | k = m + 1 - i 133 | g_ = n * x ** i * y ** j * z ** k 134 | shifts.append(g_) 135 | 136 | for i in range(m + 1): 137 | j = m + t + 1 - i 138 | for k in range(m - i + 1): 139 | h_ = n * x ** i * y ** j * z ** k 140 | shifts.append(h_) 141 | 142 | L, monomials = small_roots.create_lattice(pr, shifts, [X, Y, Z]) 143 | L = small_roots.reduce_lattice(L) 144 | polynomials = small_roots.reconstruct_polynomials(L, f, n, monomials, [X, Y, Z]) 145 | for roots in small_roots.find_roots(pr, [f] + polynomials, method=roots_method): 146 | yield roots[x], roots[y], roots[z] 147 | -------------------------------------------------------------------------------- /shared/small_roots/herrmann_may.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sage.all import ZZ 4 | 5 | from shared import small_roots 6 | 7 | 8 | def modular_bivariate(f, e, m, t, X, Y, roots_method="groebner"): 9 | """ 10 | Computes small modular roots of a bivariate polynomial. 11 | More information: Herrmann M., May A., "Maximizing Small Root Bounds by Linearization and Applications to Small Secret Exponent RSA" 12 | :param f: the polynomial 13 | :param e: the modulus 14 | :param m: the amount of normal shifts to use 15 | :param t: the amount of additional shifts to use 16 | :param X: an approximate bound on the x roots 17 | :param Y: an approximate bound on the y roots 18 | :param roots_method: the method to use to find roots (default: "groebner") 19 | :return: a generator generating small roots (tuples of x and y roots) of the polynomial 20 | """ 21 | f = f.change_ring(ZZ) 22 | 23 | pr = ZZ["x", "y", "u"] 24 | x, y, u = pr.gens() 25 | qr = pr.quotient(1 + x * y - u) 26 | U = X * Y 27 | 28 | logging.debug("Generating shifts...") 29 | 30 | shifts = [] 31 | for k in range(m + 1): 32 | for i in range(m - k + 1): 33 | g = x ** i * f ** k * e ** (m - k) 34 | g = qr(g).lift() 35 | shifts.append(g) 36 | 37 | for j in range(1, t + 1): 38 | for k in range(m // t * j, m + 1): 39 | h = y ** j * f ** k * e ** (m - k) 40 | h = qr(h).lift() 41 | shifts.append(h) 42 | 43 | L, monomials = small_roots.create_lattice(pr, shifts, [X, Y, U]) 44 | L = small_roots.reduce_lattice(L) 45 | 46 | pr = f.parent() 47 | x, y = pr.gens() 48 | 49 | polynomials = small_roots.reconstruct_polynomials(L, f, None, monomials, [X, Y, U], preprocess_polynomial=lambda p: p(x, y, 1 + x * y)) 50 | for roots in small_roots.find_roots(pr, polynomials, method=roots_method): 51 | yield roots[x], roots[y] 52 | -------------------------------------------------------------------------------- /shared/small_roots/herrmann_may_multivariate.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from math import gcd 3 | 4 | from sage.all import ZZ 5 | 6 | from shared import small_roots 7 | 8 | 9 | def _get_shifts(m, x, k, shift, j, sum, shifts): 10 | if j == len(x): 11 | shifts.append(shift) 12 | else: 13 | for ij in range(m + 1 - k - sum): 14 | _get_shifts(m, x, k, shift * x[j] ** ij, j + 1, sum + ij, shifts) 15 | 16 | 17 | def modular_multivariate(f, N, m, t, X, roots_method="groebner"): 18 | """ 19 | Computes small modular roots of a multivariate polynomial. 20 | More information: Herrmann M., May A., "Solving Linear Equations Modulo Divisors: On Factoring Given Any Bits" (Section 3 and 4) 21 | :param f: the polynomial 22 | :param N: the modulus 23 | :param m: the the parameter m 24 | :param t: the the parameter t 25 | :param X: a list of approximate bounds on the roots for each variable 26 | :param roots_method: the method to use to find roots (default: "groebner") 27 | :return: a generator generating small roots (tuples) of the polynomial 28 | """ 29 | f = f.change_ring(ZZ) 30 | pr = f.parent() 31 | x = pr.gens() 32 | 33 | # Sage lm method depends on the term ordering 34 | l = 1 35 | for monomial in f.monomials(): 36 | if monomial % l == 0: 37 | l = monomial 38 | 39 | al = int(f.coefficient(l)) 40 | assert gcd(al, N) == 1 41 | f_ = (pow(al, -1, N) * f % N).change_ring(ZZ) 42 | 43 | logging.debug("Generating shifts...") 44 | 45 | shifts = [] 46 | for k in range(m + 1): 47 | _get_shifts(m, x, k, f_ ** k * N ** max(t - k, 0), 1, 0, shifts) 48 | 49 | L, monomials = small_roots.create_lattice(pr, shifts, X) 50 | L = small_roots.reduce_lattice(L) 51 | polynomials = small_roots.reconstruct_polynomials(L, f, N, monomials, X) 52 | for roots in small_roots.find_roots(pr, polynomials, method=roots_method): 53 | yield tuple(roots[xi] for xi in x) 54 | -------------------------------------------------------------------------------- /shared/small_roots/howgrave_graham.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sage.all import ZZ 4 | 5 | from shared import small_roots 6 | 7 | 8 | def modular_univariate(f, N, m, t, X): 9 | """ 10 | Computes small modular roots of a univariate polynomial. 11 | More information: May A., "New RSA Vulnerabilities Using Lattice Reduction Methods" (Section 3.2) 12 | :param f: the polynomial 13 | :param N: the modulus 14 | :param m: the amount of normal shifts to use 15 | :param t: the amount of additional shifts to use 16 | :param X: an approximate bound on the roots 17 | :return: a generator generating small roots of the polynomial 18 | """ 19 | f = f.monic().change_ring(ZZ) 20 | pr = f.parent() 21 | x = pr.gen() 22 | delta = f.degree() 23 | 24 | logging.debug("Generating shifts...") 25 | 26 | shifts = [] 27 | for i in range(m): 28 | for j in range(delta): 29 | g = x ** j * N ** (m - i) * f ** i 30 | shifts.append(g) 31 | 32 | for i in range(t): 33 | h = x ** i * f ** m 34 | shifts.append(h) 35 | 36 | L, monomials = small_roots.create_lattice(pr, shifts, [X], order=None) 37 | L = small_roots.reduce_lattice(L) 38 | polynomials = small_roots.reconstruct_polynomials(L, f, N ** m, monomials, [X]) 39 | for roots in small_roots.find_roots(pr, polynomials): 40 | yield roots[x], 41 | -------------------------------------------------------------------------------- /shared/small_roots/jochemsz_may_integer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABCMeta 3 | from abc import abstractmethod 4 | from math import gcd 5 | 6 | from sage.all import ZZ 7 | 8 | from shared import small_roots 9 | 10 | 11 | class Strategy(metaclass=ABCMeta): 12 | @abstractmethod 13 | def generate_S_M(self, f, m): 14 | """ 15 | Generates the S and M sets. 16 | :param f: the polynomial 17 | :param m: the amount of normal shifts to use 18 | :return: a tuple containing the S and M sets 19 | """ 20 | pass 21 | 22 | 23 | class BasicStrategy(Strategy): 24 | def generate_S_M(self, f, m): 25 | S = set((f ** (m - 1)).monomials()) 26 | M = set((f ** m).monomials()) 27 | return S, M 28 | 29 | 30 | class ExtendedStrategy(Strategy): 31 | def __init__(self, t): 32 | self.t = t 33 | 34 | def generate_S_M(self, f, m): 35 | x = f.parent().gens() 36 | assert len(x) == len(self.t) 37 | 38 | S = set() 39 | for monomial in (f ** (m - 1)).monomials(): 40 | for xi, ti in zip(x, self.t): 41 | for j in range(ti + 1): 42 | S.add(monomial * xi ** j) 43 | 44 | M = set() 45 | for monomial in S: 46 | M.update((monomial * f).monomials()) 47 | 48 | return S, M 49 | 50 | 51 | class Ernst1Strategy(Strategy): 52 | def __init__(self, t): 53 | self.t = t 54 | 55 | def generate_S_M(self, f, m): 56 | x1, x2, x3 = f.parent().gens() 57 | 58 | S = set() 59 | for i1 in range(m): 60 | for i2 in range(m - i1): 61 | for i3 in range(i2 + self.t + 1): 62 | S.add(x1 ** i1 * x2 ** i2 * x3 ** i3) 63 | 64 | M = set() 65 | for i1 in range(m + 1): 66 | for i2 in range(m - i1 + 1): 67 | for i3 in range(i2 + self.t + 1): 68 | M.add(x1 ** i1 * x2 ** i2 * x3 ** i3) 69 | 70 | return S, M 71 | 72 | 73 | class Ernst2Strategy(Strategy): 74 | def __init__(self, t): 75 | self.t = t 76 | 77 | def generate_S_M(self, f, m): 78 | x1, x2, x3 = f.parent().gens() 79 | 80 | S = set() 81 | for i1 in range(m): 82 | for i2 in range(m - i1 + self.t): 83 | for i3 in range(m - i1): 84 | S.add(x1 ** i1 * x2 ** i2 * x3 ** i3) 85 | 86 | M = set() 87 | for i1 in range(m + 1): 88 | for i2 in range(m - i1 + self.t + 1): 89 | for i3 in range(m - i1 + 1): 90 | M.add(x1 ** i1 * x2 ** i2 * x3 ** i3) 91 | 92 | return S, M 93 | 94 | 95 | def integer_multivariate(f, m, W, X, strategy, roots_method="resultants"): 96 | """ 97 | Computes small integer roots of a multivariate polynomial. 98 | More information: Jochemsz E., May A., "A Strategy for Finding Roots of Multivariate Polynomials with New Applications in Attacking RSA Variants" (Section 2.2) 99 | :param f: the polynomial 100 | :param m: the parameter m 101 | :param W: the parameter W 102 | :param X: a list of approximate bounds on the roots for each variable 103 | :param strategy: the strategy to use (Appendix B) 104 | :param roots_method: the method to use to find roots (default: "resultants") 105 | :return: a generator generating small roots (tuples) of the polynomial 106 | """ 107 | pr = f.parent() 108 | x = pr.gens() 109 | assert len(x) > 1 110 | 111 | S, M = strategy.generate_S_M(f, m) 112 | l = [0] * len(x) 113 | for monomial in S: 114 | for j, xj in enumerate(x): 115 | l[j] = max(l[j], monomial.degree(xj)) 116 | 117 | a0 = int(f.constant_coefficient()) 118 | assert a0 != 0 119 | while gcd(a0, W) != 1: 120 | W += 1 121 | 122 | R = W 123 | for j, Xj in enumerate(X): 124 | while gcd(a0, Xj) != 1: 125 | Xj += 1 126 | 127 | R *= Xj ** l[j] 128 | X[j] = Xj 129 | 130 | assert gcd(a0, R) == 1 131 | f_ = (pow(a0, -1, R) * f % R).change_ring(ZZ) 132 | 133 | logging.debug("Generating shifts...") 134 | 135 | shifts = [] 136 | monomials = set() 137 | for monomial in S: 138 | g = monomial * f_ 139 | for xj, Xj, lj in zip(x, X, l): 140 | g *= Xj ** (lj - monomial.degree(xj)) 141 | 142 | shifts.append(g) 143 | monomials.add(monomial) 144 | 145 | for monomial in M: 146 | if monomial not in S: 147 | shifts.append(monomial * R) 148 | monomials.add(monomial) 149 | 150 | L, monomials = small_roots.create_lattice(pr, shifts, X) 151 | L = small_roots.reduce_lattice(L) 152 | polynomials = small_roots.reconstruct_polynomials(L, f, R, monomials, X) 153 | for roots in small_roots.find_roots(pr, [f] + polynomials, method=roots_method): 154 | yield tuple(roots[xi] for xi in x) 155 | -------------------------------------------------------------------------------- /shared/small_roots/jochemsz_may_modular.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABCMeta 3 | from abc import abstractmethod 4 | from math import gcd 5 | 6 | from sage.all import ZZ 7 | 8 | from shared import small_roots 9 | 10 | 11 | class Strategy(metaclass=ABCMeta): 12 | @abstractmethod 13 | def generate_M(self, f, l, m): 14 | """ 15 | Generates the M dict. 16 | :param f: the polynomial 17 | :param l: the leading monomial 18 | :param m: the amount of normal shifts to use 19 | :return: the M dict 20 | """ 21 | pass 22 | 23 | 24 | class BasicStrategy(Strategy): 25 | def generate_M(self, f, l, m): 26 | M = {} 27 | fm_monomials = (f ** m).monomials() 28 | for k in range(m + 1): 29 | M[k] = set() 30 | fmk_monomials = (f ** (m - k)).monomials() 31 | for monomial in fm_monomials: 32 | if monomial // (l ** k) in fmk_monomials: 33 | M[k].add(monomial) 34 | 35 | M[m + 1] = [] 36 | return M 37 | 38 | 39 | class ExtendedStrategy(Strategy): 40 | def __init__(self, t): 41 | self.t = t 42 | 43 | def generate_M(self, f, l, m): 44 | x = f.parent().gens() 45 | assert len(x) == len(self.t) 46 | 47 | M = {} 48 | fm_monomials = (f ** m).monomials() 49 | for k in range(m + 1): 50 | M[k] = set() 51 | fmk_monomials = (f ** (m - k)).monomials() 52 | for monomial in fm_monomials: 53 | if monomial // (l ** k) in fmk_monomials: 54 | for xi, ti in zip(x, self.t): 55 | for j in range(ti + 1): 56 | M[k].add(monomial * xi ** j) 57 | 58 | M[m + 1] = [] 59 | return M 60 | 61 | 62 | class BonehDurfeeStrategy(Strategy): 63 | def __init__(self, t): 64 | self.t = t 65 | 66 | def generate_M(self, f, l, m): 67 | x1, x2 = f.parent().gens() 68 | 69 | M = {} 70 | for k in range(m + 1): 71 | M[k] = set() 72 | for i1 in range(k, m + 1): 73 | for i2 in range(k, i1 + self.t + 1): 74 | M[k].add(x1 ** i1 * x2 ** i2) 75 | 76 | M[m + 1] = [] 77 | return M 78 | 79 | 80 | class BlomerMayStrategy(Strategy): 81 | def __init__(self, t): 82 | self.t = t 83 | 84 | def generate_M(self, f, l, m): 85 | x1, x2, x3 = f.parent().gens() 86 | 87 | M = {} 88 | for k in range(m + 1): 89 | M[k] = set() 90 | for i1 in range(k, m + 1): 91 | for i2 in range(m - i1 + 1): 92 | for i3 in range(i2 + self.t - 1): 93 | M[k].add(x1 ** i1 * x2 ** i2 * x3 ** i3) 94 | 95 | M[m + 1] = [] 96 | return M 97 | 98 | 99 | def modular_multivariate(f, N, m, X, strategy, roots_method="groebner"): 100 | """ 101 | Computes small integer roots of a multivariate polynomial. 102 | More information: Jochemsz E., May A., "A Strategy for Finding Roots of Multivariate Polynomials with New Applications in Attacking RSA Variants" (Section 2.1) 103 | :param f: the polynomial 104 | :param N: the modulus 105 | :param m: the parameter m 106 | :param X: a list of approximate bounds on the roots for each variable 107 | :param strategy: the strategy to use (Appendix A) 108 | :param roots_method: the method to use to find roots (default: "groebner") 109 | :return: a generator generating small roots (tuples) of the polynomial 110 | """ 111 | f = f.change_ring(ZZ) 112 | pr = f.parent() 113 | x = pr.gens() 114 | assert len(x) > 1 115 | 116 | # Sage lm method depends on the term ordering 117 | l = 1 118 | for monomial in f.monomials(): 119 | if monomial % l == 0: 120 | l = monomial 121 | 122 | al = int(f.coefficient(l)) 123 | assert gcd(al, N) == 1 124 | f_ = (pow(al, -1, N) * f % N).change_ring(ZZ) 125 | 126 | logging.debug("Generating shifts...") 127 | 128 | M = strategy.generate_M(f, l, m) 129 | shifts = [] 130 | monomials = set() 131 | for k in range(m + 1): 132 | for monomial in M[k]: 133 | if monomial not in M[k + 1]: 134 | shifts.append(monomial // (l ** k) * f_ ** k * N ** (m - k)) 135 | monomials.add(monomial) 136 | 137 | L, monomials = small_roots.create_lattice(pr, shifts, X) 138 | L = small_roots.reduce_lattice(L) 139 | polynomials = small_roots.reconstruct_polynomials(L, f, N ** m, monomials, X) 140 | for roots in small_roots.find_roots(pr, polynomials, method=roots_method): 141 | yield tuple(roots[xi] for xi in x) 142 | -------------------------------------------------------------------------------- /shared/small_roots/nitaj_fouotsa.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sage.all import ZZ 4 | 5 | from shared import small_roots 6 | 7 | 8 | def modular_trivariate(f, e, m, t, X, Y, Z, roots_method="groebner"): 9 | """ 10 | Computes small modular roots of a trivariate polynomial. 11 | More information: Nitaj A., Fouotsa E., "A New Attack on RSA and Demytko's Elliptic Curve Cryptosystem" (Section 3) 12 | :param f: the polynomial 13 | :param e: the modulus 14 | :param m: the parameter m 15 | :param t: the parameter t 16 | :param X: an approximate bound on the x roots 17 | :param Y: an approximate bound on the y roots 18 | :param Z: an approximate bound on the z roots 19 | :param roots_method: the method to use to find roots (default: "groebner") 20 | :return: a generator generating small roots (tuples of x, y, and z roots) of the polynomial 21 | """ 22 | f = f.change_ring(ZZ) 23 | pr = f.parent() 24 | x, y, z = pr.gens() 25 | 26 | logging.debug("Generating shifts...") 27 | 28 | shifts = [] 29 | for k in range(m + 1): 30 | for i1 in range(k, m + 1): 31 | i2 = k 32 | i3 = m - i1 33 | g = x ** (i1 - k) * z ** i3 * f ** k * e ** (m - k) 34 | shifts.append(g) 35 | 36 | i1 = k 37 | for i2 in range(k + 1, i1 + t + 1): 38 | i3 = m - i1 39 | h = y ** (i2 - k) * z ** i3 * f ** k * e ** (m - k) 40 | shifts.append(h) 41 | 42 | L, monomials = small_roots.create_lattice(pr, shifts, [X, Y, Z]) 43 | L = small_roots.reduce_lattice(L) 44 | polynomials = small_roots.reconstruct_polynomials(L, f, e ** m, monomials, [X, Y, Z]) 45 | for roots in small_roots.find_roots(pr, polynomials, method=roots_method): 46 | yield roots[x], roots[y], roots[z] 47 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvdsn/crypto-attacks/708536c43fa632bb1278edd6651a30e8743e8d21/test/__init__.py -------------------------------------------------------------------------------- /test/shared/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/shared/test_shared.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from unittest import TestCase 4 | 5 | from sage.all import GF 6 | 7 | path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__))))) 8 | if sys.path[1] != path: 9 | sys.path.insert(1, path) 10 | 11 | from shared import rth_roots 12 | 13 | 14 | class TestShared(TestCase): 15 | def test_rth_roots(self): 16 | q = 9908484735485245740582755998843475068910570989512225739800304203500256711207262150930812622460031920899674919818007279858208368349928684334780223996774347 17 | c = 7267288183214469410349447052665186833632058119533973432573869246434984462336560480880459677870106195135869371300420762693116774837763418518542884912967719 18 | e = 21 19 | self.assertEqual(len(set(rth_roots(GF(q), c, e))), 7) 20 | -------------------------------------------------------------------------------- /test/test_acd.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from unittest import TestCase 4 | 5 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 6 | if sys.path[1] != path: 7 | sys.path.insert(1, path) 8 | 9 | from attacks.acd import mp 10 | from attacks.acd import ol 11 | from attacks.acd import sda 12 | 13 | 14 | class TestACD(TestCase): 15 | def test_mp(self): 16 | p = 13845249886873428613 17 | N = 217407154917807470895390029782610665191 18 | a = [217533070209246692596654921907507416418, 204580044578352963965866243184761160753, 140701572771317740546493579818249256489, 204965623030303278851699918560590728481] 19 | r = [-4487, -10300, -10075, 23477] 20 | rho = 16 21 | p_, r_ = mp.attack(N, a, rho) 22 | self.assertIsInstance(p_, int) 23 | self.assertEqual(p, p_) 24 | for i in range(len(r)): 25 | self.assertIsInstance(r_[i], int) 26 | self.assertEqual(r[i], r_[i]) 27 | 28 | p = 4238219929 29 | N = 169749234656568819546953289884491257755 30 | a = [244914656571365600647675250712402894810, 258938327120706515107425576685234055392, 304762109988168193461144822345045328384, 331420653433993738593271242794196239800, 213192828695455264854663438180715919066, 178555459224222734102095815362417919512, 240756376346047770614611034188210416410, 168622111253832154566397598758889653217] 31 | r = [48611, 26045, -62765, -18028, -9003, -23809, -42845, -45828] 32 | rho = 16 33 | p_, r_ = mp.attack(N, a, rho) 34 | self.assertIsInstance(p_, int) 35 | self.assertEqual(p, p_) 36 | for i in range(len(r)): 37 | self.assertIsInstance(r_[i], int) 38 | self.assertEqual(r[i], r_[i]) 39 | 40 | def test_ol(self): 41 | p = 13845249886873428613 42 | x = [217533070209246692596654921907507416418, 204580044578352963965866243184761160753, 140701572771317740546493579818249256489, 204965623030303278851699918560590728481] 43 | r = [-4487, -10300, -10075, 23477] 44 | rho = 16 45 | p_, r_ = ol.attack(x, rho) 46 | self.assertIsInstance(p_, int) 47 | self.assertEqual(p, p_) 48 | for i in range(len(r)): 49 | self.assertIsInstance(r_[i], int) 50 | self.assertEqual(r[i], r_[i]) 51 | 52 | p = 4238219929 53 | x = [244914656571365600647675250712402894810, 258938327120706515107425576685234055392, 304762109988168193461144822345045328384, 331420653433993738593271242794196239800, 213192828695455264854663438180715919066, 178555459224222734102095815362417919512, 240756376346047770614611034188210416410, 168622111253832154566397598758889653217] 54 | r = [48611, 26045, -62765, -18028, -9003, -23809, -42845, -45828] 55 | rho = 16 56 | p_, r_ = ol.attack(x, rho) 57 | self.assertIsInstance(p_, int) 58 | self.assertEqual(p, p_) 59 | for i in range(len(r)): 60 | self.assertIsInstance(r_[i], int) 61 | self.assertEqual(r[i], r_[i]) 62 | 63 | def test_sda(self): 64 | p = 13845249886873428613 65 | x = [217533070209246692596654921907507416418, 204580044578352963965866243184761160753, 140701572771317740546493579818249256489, 204965623030303278851699918560590728481] 66 | r = [-4487, -10300, -10075, 23477] 67 | rho = 16 68 | p_, r_ = sda.attack(x, rho) 69 | self.assertIsInstance(p_, int) 70 | self.assertEqual(p, p_) 71 | for i in range(len(r)): 72 | self.assertIsInstance(r_[i], int) 73 | self.assertEqual(r[i], r_[i]) 74 | 75 | p = 4238219929 76 | x = [244914656571365600647675250712402894810, 258938327120706515107425576685234055392, 304762109988168193461144822345045328384, 331420653433993738593271242794196239800, 213192828695455264854663438180715919066, 178555459224222734102095815362417919512, 240756376346047770614611034188210416410, 168622111253832154566397598758889653217] 77 | r = [48611, 26045, -62765, -18028, -9003, -23809, -42845, -45828] 78 | rho = 16 79 | p_, r_ = sda.attack(x, rho) 80 | self.assertIsInstance(p_, int) 81 | self.assertEqual(p, p_) 82 | for i in range(len(r)): 83 | self.assertIsInstance(r_[i], int) 84 | self.assertEqual(r[i], r_[i]) 85 | -------------------------------------------------------------------------------- /test/test_cbc.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from random import randbytes 4 | from unittest import TestCase 5 | 6 | from Crypto.Cipher import AES 7 | from Crypto.Util.Padding import pad 8 | from Crypto.Util.Padding import unpad 9 | 10 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 11 | if sys.path[1] != path: 12 | sys.path.insert(1, path) 13 | 14 | from attacks.cbc import bit_flipping 15 | from attacks.cbc import iv_recovery 16 | from attacks.cbc import padding_oracle 17 | 18 | 19 | class TestCBC(TestCase): 20 | def _encrypt(self, key, p): 21 | iv = randbytes(16) 22 | cipher = AES.new(key, mode=AES.MODE_CBC, iv=iv) 23 | c = cipher.encrypt(p) 24 | return iv, c 25 | 26 | def _decrypt(self, key, iv, c): 27 | cipher = AES.new(key, mode=AES.MODE_CBC, iv=iv) 28 | p = cipher.decrypt(c) 29 | return p 30 | 31 | def _valid_padding(self, key, iv, c): 32 | try: 33 | unpad(self._decrypt(key, iv, c), 16) 34 | return True 35 | except ValueError: 36 | return False 37 | 38 | def test_bit_flipping(self): 39 | key = randbytes(16) 40 | p = randbytes(32) 41 | p_ = randbytes(16) 42 | iv, c = self._encrypt(key, p) 43 | 44 | iv_, c_ = bit_flipping.attack(iv, c, 0, p[0:len(p_)], p_) 45 | p__ = self._decrypt(key, iv_, c_) 46 | self.assertEqual(p_, p__[0:len(p_)]) 47 | 48 | iv_, c_ = bit_flipping.attack(iv, c, 16, p[16:16 + len(p_)], p_) 49 | p__ = self._decrypt(key, iv_, c_) 50 | self.assertEqual(p_, p__[16:16 + len(p_)]) 51 | 52 | def test_iv_recovery(self): 53 | key = randbytes(16) 54 | iv = randbytes(16) 55 | iv_ = iv_recovery.attack(lambda c: self._decrypt(key, iv, c)) 56 | self.assertEqual(iv, iv_) 57 | 58 | def test_padding_oracle(self): 59 | key = randbytes(16) 60 | for i in range(16): 61 | p = pad(randbytes(i + 1), 16) 62 | iv, c = self._encrypt(key, p) 63 | p_ = padding_oracle.attack(lambda iv, c: self._valid_padding(key, iv, c), iv, c) 64 | self.assertEqual(p, p_) 65 | -------------------------------------------------------------------------------- /test/test_cbc_and_cbc_mac.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from random import randbytes 4 | from unittest import TestCase 5 | 6 | from Crypto.Cipher import AES 7 | from Crypto.Util.Padding import pad 8 | from Crypto.Util.Padding import unpad 9 | 10 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 11 | if sys.path[1] != path: 12 | sys.path.insert(1, path) 13 | 14 | from attacks.cbc_and_cbc_mac import eam_key_reuse 15 | from attacks.cbc_and_cbc_mac import etm_key_reuse 16 | from attacks.cbc_and_cbc_mac import mte_key_reuse 17 | 18 | 19 | class TestCBCAndCBCMAC(TestCase): 20 | def _encrypt_eam(self, key, p): 21 | # Notice how the key is used for encryption and authentication... 22 | p = pad(p, 16) 23 | iv = randbytes(16) 24 | c = AES.new(key, AES.MODE_CBC, iv).encrypt(p) 25 | # Encrypt-and-MAC using CBC-MAC to prevent chosen-ciphertext attacks. 26 | t = AES.new(key, AES.MODE_CBC, bytes(16)).encrypt(p)[-16:] 27 | return iv, c, t 28 | 29 | def _decrypt_eam(self, key, iv, c, t): 30 | p = AES.new(key, AES.MODE_CBC, iv).decrypt(c) 31 | t_ = AES.new(key, AES.MODE_CBC, bytes(16)).encrypt(p)[-16:] 32 | # Check the MAC to be sure the message isn't forged. 33 | if t != t_: 34 | return None 35 | 36 | return unpad(p, 16) 37 | 38 | def _encrypt_etm(self, key, p): 39 | # Notice how the key is used for encryption and authentication... 40 | p = pad(p, 16) 41 | iv = randbytes(16) 42 | c = AES.new(key, AES.MODE_CBC, iv).encrypt(p) 43 | # Encrypt-then-MAC using CBC-MAC to prevent chosen-ciphertext attacks. 44 | t = AES.new(key, AES.MODE_CBC, bytes(16)).encrypt(iv + c)[-16:] 45 | return iv, c, t 46 | 47 | def _decrypt_etm(self, key, iv, c, t): 48 | t_ = AES.new(key, AES.MODE_CBC, bytes(16)).encrypt(iv + c)[-16:] 49 | # Check the MAC to be sure the message isn't forged. 50 | if t != t_: 51 | return None 52 | 53 | return unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(c), 16) 54 | 55 | def _encrypted_zeroes(self, key): 56 | return AES.new(key, AES.MODE_ECB).encrypt(bytes(16)) 57 | 58 | def _encrypt_mte(self, key, p): 59 | # Notice how the key is used for encryption and authentication... 60 | p = pad(p, 16) 61 | iv = randbytes(16) 62 | # MAC-then-encrypt using CBC-MAC to prevent chosen-ciphertext attacks. 63 | t = AES.new(key, AES.MODE_CBC, bytes(16)).encrypt(p)[-16:] 64 | c = AES.new(key, AES.MODE_CBC, iv).encrypt(p + t) 65 | return iv, c 66 | 67 | def _decrypt_mte(self, key, iv, c): 68 | d = AES.new(key, AES.MODE_CBC, iv).decrypt(c) 69 | p = d[:-16] 70 | t = d[-16:] 71 | t_ = AES.new(key, AES.MODE_CBC, bytes(16)).encrypt(p)[-16:] 72 | # Check the MAC to be sure the message isn't forged. 73 | if t != t_: 74 | return None 75 | 76 | return unpad(p, 16) 77 | 78 | def test_eam_key_reuse(self): 79 | key = randbytes(16) 80 | for i in range(16): 81 | p = randbytes(i + 1) 82 | iv, c, t = self._encrypt_eam(key, p) 83 | p_ = eam_key_reuse.attack(lambda iv, c, t: self._decrypt_eam(key, iv, c, t), iv, c, t) 84 | self.assertEqual(p, p_) 85 | 86 | def test_etm_key_reuse(self): 87 | key = randbytes(16) 88 | for i in range(16): 89 | p = randbytes(i + 1) 90 | iv, c, t = self._encrypt_etm(key, p) 91 | p_ = etm_key_reuse.attack(lambda p: self._encrypt_etm(key, p), lambda iv, c, t: self._decrypt_etm(key, iv, c, t), iv, c, t) 92 | self.assertEqual(p, p_) 93 | 94 | def test_mte_key_reuse(self): 95 | key = randbytes(16) 96 | encrypted_zeroes = self._encrypted_zeroes(key) 97 | for i in range(16): 98 | p = randbytes(i + 1) 99 | iv, c = self._encrypt_mte(key, p) 100 | p_ = mte_key_reuse.attack(lambda iv, c: self._decrypt_mte(key, iv, c), iv, c, encrypted_zeroes) 101 | self.assertEqual(p, p_) 102 | -------------------------------------------------------------------------------- /test/test_cbc_mac.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from random import randbytes 4 | from unittest import TestCase 5 | 6 | from Crypto.Cipher import AES 7 | 8 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 9 | if sys.path[1] != path: 10 | sys.path.insert(1, path) 11 | 12 | from attacks.cbc_mac import length_extension 13 | 14 | 15 | class TestCBCMAC(TestCase): 16 | def _compute_tag(self, key, m): 17 | return AES.new(key, AES.MODE_CBC, bytes(16)).encrypt(m)[-16:] 18 | 19 | def _verify_tag(self, key, m, t): 20 | t_ = AES.new(key, AES.MODE_CBC, bytes(16)).encrypt(m)[-16:] 21 | return t == t_ 22 | 23 | def test_length_extension(self): 24 | key = randbytes(16) 25 | m1 = randbytes(32) 26 | t1 = self._compute_tag(key, m1) 27 | m2 = randbytes(32) 28 | t2 = self._compute_tag(key, m2) 29 | 30 | m3, t3 = length_extension.attack(m1, t1, m2, t2) 31 | self.assertTrue(self._verify_tag(key, m3, t3)) 32 | -------------------------------------------------------------------------------- /test/test_ctr.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from random import randbytes 4 | from random import randint 5 | from unittest import TestCase 6 | 7 | from Crypto.Cipher import AES 8 | from Crypto.Util import Counter 9 | 10 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 11 | if sys.path[1] != path: 12 | sys.path.insert(1, path) 13 | 14 | from attacks.ctr import bit_flipping 15 | from attacks.ctr import separator_oracle 16 | 17 | 18 | class TestCTR(TestCase): 19 | def _encrypt(self, key, p): 20 | return AES.new(key, AES.MODE_CTR, counter=Counter.new(128)).encrypt(p) 21 | 22 | def _decrypt(self, key, p): 23 | return AES.new(key, AES.MODE_CTR, counter=Counter.new(128)).decrypt(p) 24 | 25 | def _valid_separators(self, separator_byte, separator_count, key, c): 26 | p = AES.new(key, AES.MODE_CTR, counter=Counter.new(128)).decrypt(c) 27 | return p.count(separator_byte) == separator_count 28 | 29 | def test_bit_flipping(self): 30 | key = randbytes(16) 31 | p = randbytes(32) 32 | p_ = randbytes(16) 33 | c = self._encrypt(key, p) 34 | 35 | c_ = bit_flipping.attack(c, 0, p[0:len(p_)], p_) 36 | p__ = self._decrypt(key, c_) 37 | self.assertEqual(p_, p__[0:len(p_)]) 38 | 39 | c_ = bit_flipping.attack(c, 16, p[16:16 + len(p_)], p_) 40 | p__ = self._decrypt(key, c_) 41 | self.assertEqual(p_, p__[16:16 + len(p_)]) 42 | 43 | def test_crime(self): 44 | # TODO: CRIME attack is too inconsistent in unit tests. 45 | pass 46 | 47 | def test_separator_oracle(self): 48 | separator_byte = ord("\x00") 49 | separator_count = randint(1, 10) 50 | key = randbytes(16) 51 | # We have to replace separators by some other byte. 52 | p = randbytes(16).replace(b"\x00", b"\x01") 53 | for _ in range(separator_count): 54 | # We have to replace separators by some other byte. 55 | p += bytes([separator_byte]) + randbytes(16).replace(b"\x00", b"\x01") 56 | 57 | c = self._encrypt(key, p) 58 | 59 | p_ = separator_oracle.attack(lambda c: self._valid_separators(separator_byte, separator_count, key, c), separator_byte, c) 60 | self.assertEqual(p, p_) 61 | -------------------------------------------------------------------------------- /test/test_ecb.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from random import choices 4 | from random import randrange 5 | from unittest import TestCase 6 | 7 | from Crypto.Cipher import AES 8 | from Crypto.Util.Padding import pad 9 | 10 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 11 | if sys.path[1] != path: 12 | sys.path.insert(1, path) 13 | 14 | from attacks.ecb import plaintext_recovery 15 | from attacks.ecb import plaintext_recovery_harder 16 | from attacks.ecb import plaintext_recovery_hardest 17 | 18 | 19 | class TestECB(TestCase): 20 | bytes = list(range(1, 256)) 21 | 22 | def _randbytes(self, k): 23 | return bytes(choices(self.bytes, k=k)) 24 | 25 | def _encrypt(self, key, p): 26 | return AES.new(key, AES.MODE_ECB).encrypt(p) 27 | 28 | def test_plaintext_recovery(self): 29 | key = self._randbytes(16) 30 | for i in [0, 1, 2, 15, 16, 17, 31, 32]: 31 | s = self._randbytes(i) 32 | s_ = plaintext_recovery.attack(lambda p: self._encrypt(key, pad(p + s, 16))) 33 | self.assertEqual(s, s_) 34 | 35 | def test_plaintext_recovery_harder(self): 36 | key = self._randbytes(16) 37 | for i in range(16): 38 | prefix = self._randbytes(i) 39 | for j in [0, 1, 2, 15, 16, 17, 31, 32]: 40 | s = self._randbytes(j) 41 | s_ = plaintext_recovery_harder.attack(lambda p: self._encrypt(key, pad(prefix + p + s, 16))) 42 | self.assertEqual(s, s_) 43 | 44 | def test_plaintext_recovery_hardest(self): 45 | key = self._randbytes(16) 46 | for i in [0, 1, 2, 15, 16, 17, 31, 32]: 47 | s = self._randbytes(i) 48 | s_ = plaintext_recovery_hardest.attack(lambda p: self._encrypt(key, pad(self._randbytes(randrange(0, 16)) + p + s, 16))) 49 | self.assertEqual(s, s_) 50 | -------------------------------------------------------------------------------- /test/test_elgamal_encryption.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from random import randrange 4 | from unittest import TestCase 5 | 6 | from sage.all import legendre_symbol 7 | 8 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 9 | if sys.path[1] != path: 10 | sys.path.insert(1, path) 11 | 12 | from attacks.elgamal_encryption import nonce_reuse 13 | from attacks.elgamal_encryption import unsafe_generator 14 | 15 | 16 | class TestElgamalEncryption(TestCase): 17 | def test_nonce_reuse(self): 18 | # Safe prime. 19 | p = 16902648776703029279 20 | g = 3 21 | for _ in range(100): 22 | x = randrange(1, p) 23 | h = pow(g, x, p) 24 | y = randrange(1, p) 25 | s = pow(h, y, p) 26 | m = randrange(1, p) 27 | c1 = pow(g, y, p) 28 | c2 = m * s % p 29 | m_ = randrange(1, p) 30 | c1_ = pow(g, y, p) 31 | c2_ = m_ * s % p 32 | m__ = nonce_reuse.attack(p, m, c1, c2, c1_, c2_) 33 | self.assertIsInstance(m__, int) 34 | self.assertEqual(m_, m__) 35 | 36 | def test_unsafe_generator(self): 37 | # Safe prime. 38 | p = 16902648776703029279 39 | # Unsafe generator, generates the entire group. 40 | g = 7 41 | for _ in range(100): 42 | x = randrange(1, p) 43 | h = pow(g, x, p) 44 | y = randrange(1, p) 45 | s = pow(h, y, p) 46 | m = randrange(1, p) 47 | c1 = pow(g, y, p) 48 | c2 = m * s % p 49 | k = unsafe_generator.attack(p, h, c1, c2) 50 | self.assertIsInstance(k, int) 51 | self.assertEqual(legendre_symbol(m, p), k) 52 | -------------------------------------------------------------------------------- /test/test_elgamal_signature.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from math import gcd 4 | from random import getrandbits 5 | from random import randrange 6 | from unittest import TestCase 7 | 8 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 9 | if sys.path[1] != path: 10 | sys.path.insert(1, path) 11 | 12 | from attacks.elgamal_signature import nonce_reuse 13 | 14 | 15 | class TestElgamalSignature(TestCase): 16 | def test_nonce_reuse(self): 17 | # Safe prime. 18 | p = 16902648776703029279 19 | g = 3 20 | x = randrange(1, p - 1) 21 | k = p - 1 22 | while gcd(k, p - 1) != 1: 23 | k = randrange(2, p - 1) 24 | 25 | r = pow(g, k, p) 26 | m1 = getrandbits(p.bit_length()) 27 | s1 = pow(k, -1, p - 1) * (m1 - r * x) % (p - 1) 28 | m2 = getrandbits(p.bit_length()) 29 | s2 = pow(k, -1, p - 1) * (m2 - r * x) % (p - 1) 30 | for k_, x_ in nonce_reuse.attack(p, m1, r, s1, m2, r, s2): 31 | self.assertIsInstance(k_, int) 32 | self.assertIsInstance(x_, int) 33 | if k_ == k and x_ == x: 34 | break 35 | else: 36 | self.fail() 37 | -------------------------------------------------------------------------------- /test/test_gcm.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from random import randbytes 4 | from unittest import TestCase 5 | 6 | from Crypto.Cipher import AES 7 | 8 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 9 | if sys.path[1] != path: 10 | sys.path.insert(1, path) 11 | 12 | from attacks.gcm import forbidden_attack 13 | 14 | 15 | class TestGCM(TestCase): 16 | def test_forbidden_attack(self): 17 | # Test full GCM 18 | key = randbytes(16) 19 | iv = randbytes(16) 20 | aes = AES.new(key, AES.MODE_GCM, nonce=iv) 21 | a1 = randbytes(16) 22 | p1 = randbytes(16) 23 | aes.update(a1) 24 | c1, t1 = aes.encrypt_and_digest(p1) 25 | aes = AES.new(key, AES.MODE_GCM, nonce=iv) 26 | a2 = randbytes(16) 27 | p2 = randbytes(16) 28 | aes.update(a2) 29 | c2, t2 = aes.encrypt_and_digest(p2) 30 | for h in forbidden_attack.recover_possible_auth_keys(a1, c1, t1, a2, c2, t2): 31 | target_a = randbytes(16) 32 | target_c = randbytes(16) 33 | forged_t = forbidden_attack.forge_tag(h, a1, c1, t1, target_a, target_c) 34 | try: 35 | aes = AES.new(key, AES.MODE_GCM, nonce=iv) 36 | aes.update(target_a) 37 | aes.decrypt_and_verify(target_c, forged_t) 38 | break 39 | except ValueError: 40 | # Authentication failed, so we try the next authentication key. 41 | continue 42 | else: 43 | self.fail() 44 | 45 | # Test MAC only (sometimes known as GMAC) 46 | key = randbytes(16) 47 | iv = randbytes(16) 48 | aes = AES.new(key, AES.MODE_GCM, nonce=iv) 49 | a1 = randbytes(16) 50 | aes.update(a1) 51 | t1 = aes.digest() 52 | aes = AES.new(key, AES.MODE_GCM, nonce=iv) 53 | a2 = randbytes(16) 54 | aes.update(a2) 55 | t2 = aes.digest() 56 | for h in forbidden_attack.recover_possible_auth_keys(a1, [], t1, a2, [], t2): 57 | target_a = randbytes(16) 58 | target_c = [] 59 | forged_t = forbidden_attack.forge_tag(h, a1, [], t1, target_a, target_c) 60 | try: 61 | aes = AES.new(key, AES.MODE_GCM, nonce=iv) 62 | aes.update(target_a) 63 | aes.verify(forged_t) 64 | break 65 | except ValueError: 66 | # Authentication failed, so we try the next authentication key. 67 | continue 68 | else: 69 | self.fail() 70 | -------------------------------------------------------------------------------- /test/test_hnp.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from random import getrandbits 4 | from random import randrange 5 | from unittest import TestCase 6 | 7 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 8 | if sys.path[1] != path: 9 | sys.path.insert(1, path) 10 | 11 | from attacks.hnp import extended_hnp 12 | from attacks.hnp import lattice_attack 13 | from shared.partial_integer import PartialInteger 14 | 15 | 16 | class TestHNP(TestCase): 17 | def _dsa(self, p, g, x): 18 | h = getrandbits(p.bit_length()) 19 | k = randrange(1, p) 20 | r = pow(g, k, p) 21 | s = (pow(k, -1, p) * (h + x * r)) % p 22 | return h, r, s, k 23 | 24 | def test_extended_hnp(self): 25 | # Not a safe prime, but it doesn't really matter. 26 | p = 299182277398782807472682876223275635417 27 | g = 5 28 | x = randrange(1, p) 29 | 30 | k_bit_length = p.bit_length() 31 | lsb_unknown = 50 32 | msb_unknown = 50 33 | n_signatures = 5 34 | h = [] 35 | r = [] 36 | s = [] 37 | k = [] 38 | partial_k = [] 39 | for i in range(n_signatures): 40 | hi, ri, si, ki = self._dsa(p, g, x) 41 | h.append(hi) 42 | r.append(ri) 43 | s.append(si) 44 | k.append(ki) 45 | partial_k.append(PartialInteger.middle_of(ki, k_bit_length, lsb_unknown, msb_unknown)) 46 | 47 | x_ = next(extended_hnp.dsa_known_bits(p, h, r, s, PartialInteger.unknown(k_bit_length), partial_k)) 48 | self.assertIsInstance(x_, int) 49 | self.assertEqual(x, x_) 50 | 51 | def test_lattice_attack(self): 52 | # Not a safe prime, but it doesn't really matter. 53 | p = 299182277398782807472682876223275635417 54 | g = 5 55 | x = randrange(1, p) 56 | 57 | k_bit_length = p.bit_length() 58 | msb_known = 7 59 | n_signatures = 25 60 | h = [] 61 | r = [] 62 | s = [] 63 | k = [] 64 | partial_k = [] 65 | for i in range(n_signatures): 66 | hi, ri, si, ki = self._dsa(p, g, x) 67 | h.append(hi) 68 | r.append(ri) 69 | s.append(si) 70 | k.append(ki) 71 | partial_k.append(PartialInteger.msb_of(ki, k_bit_length, msb_known)) 72 | 73 | x_, k_ = next(lattice_attack.dsa_known_msb(p, h, r, s, partial_k)) 74 | self.assertIsInstance(x_, int) 75 | self.assertIsInstance(k_, list) 76 | self.assertEqual(x, x_) 77 | for i in range(n_signatures): 78 | self.assertIsInstance(k_[i], int) 79 | self.assertEqual(k[i], k_[i]) 80 | 81 | k_bit_length = p.bit_length() 82 | lsb_known = 7 83 | n_signatures = 25 84 | h = [] 85 | r = [] 86 | s = [] 87 | k = [] 88 | partial_k = [] 89 | for i in range(n_signatures): 90 | hi, ri, si, ki = self._dsa(p, g, x) 91 | h.append(hi) 92 | r.append(ri) 93 | s.append(si) 94 | k.append(ki) 95 | partial_k.append(PartialInteger.lsb_of(ki, k_bit_length, lsb_known)) 96 | 97 | x_, k_ = next(lattice_attack.dsa_known_lsb(p, h, r, s, partial_k)) 98 | self.assertIsInstance(x_, int) 99 | self.assertIsInstance(k_, list) 100 | self.assertEqual(x, x_) 101 | for i in range(n_signatures): 102 | self.assertIsInstance(k_[i], int) 103 | self.assertEqual(k[i], k_[i]) 104 | 105 | k_bit_length = p.bit_length() 106 | lsb_unknown = 20 107 | msb_unknown = 10 108 | h1, r1, s1, k1 = self._dsa(p, g, x) 109 | partial_k1 = PartialInteger.middle_of(k1, k_bit_length, lsb_unknown, msb_unknown) 110 | h2, r2, s2, k2 = self._dsa(p, g, x) 111 | partial_k2 = PartialInteger.middle_of(k2, k_bit_length, lsb_unknown, msb_unknown) 112 | 113 | x_, k1_, k2_ = lattice_attack.dsa_known_middle(p, h1, r1, s1, partial_k1, h2, r2, s2, partial_k2) 114 | self.assertIsInstance(x_, int) 115 | self.assertIsInstance(k1_, int) 116 | self.assertIsInstance(k2_, int) 117 | self.assertEqual(x, x_) 118 | self.assertEqual(k1, k1_) 119 | self.assertEqual(k2, k2_) 120 | -------------------------------------------------------------------------------- /test/test_ige.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from random import randbytes 4 | from unittest import TestCase 5 | 6 | from Crypto.Cipher import AES 7 | from Crypto.Util.Padding import pad 8 | from Crypto.Util.Padding import unpad 9 | from Crypto.Util.strxor import strxor 10 | 11 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 12 | if sys.path[1] != path: 13 | sys.path.insert(1, path) 14 | 15 | from attacks.ige import padding_oracle 16 | 17 | 18 | class TestIGE(TestCase): 19 | def _encrypt(self, key, p): 20 | p0 = randbytes(16) 21 | c0 = randbytes(16) 22 | cipher = AES.new(key, mode=AES.MODE_ECB) 23 | 24 | p_last = p0 25 | c_last = c0 26 | c = bytearray() 27 | for i in range(0, len(p), 16): 28 | p_i = p[i:i + 16] 29 | c_i = strxor(cipher.encrypt(strxor(p_i, c_last)), p_last) 30 | p_last = p_i 31 | c_last = c_i 32 | c += c_i 33 | 34 | return p0, c0, c 35 | 36 | def _decrypt(self, key, p0, c0, c): 37 | cipher = AES.new(key, mode=AES.MODE_ECB) 38 | p_last = p0 39 | c_last = c0 40 | p = bytearray() 41 | for i in range(0, len(c), 16): 42 | c_i = c[i:i + 16] 43 | p_i = strxor(cipher.decrypt(strxor(c_i, p_last)), c_last) 44 | p_last = p_i 45 | c_last = c_i 46 | p += p_i 47 | 48 | return p 49 | 50 | def _valid_padding(self, key, p0, c0, c): 51 | try: 52 | unpad(self._decrypt(key, p0, c0, c), 16) 53 | return True 54 | except ValueError: 55 | return False 56 | 57 | def test_padding_oracle(self): 58 | key = randbytes(16) 59 | 60 | for i in range(16): 61 | p = pad(randbytes(i + 1), 16) 62 | p0, c0, c = self._encrypt(key, p) 63 | p_ = padding_oracle.attack(lambda p0, c0, c: self._valid_padding(key, p0, c0, c), p0, c0, c) 64 | self.assertEqual(p, p_) 65 | -------------------------------------------------------------------------------- /test/test_knapsack.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from unittest import TestCase 4 | 5 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 6 | if sys.path[1] != path: 7 | sys.path.insert(1, path) 8 | 9 | from attacks.knapsack import low_density 10 | 11 | 12 | class TestKnapsack(TestCase): 13 | def test_low_density(self): 14 | a = [429970831622, 650002882675, 512682138397, 145532365100, 462119415111, 357461497167, 582429951539, 22657777498, 2451348134, 380282710854, 251660920136, 103765486463, 276100153517, 250012242739, 519736909707, 451460714161] 15 | s = 5398327344820 16 | e = low_density.attack(a, s) 17 | for i in range(len(a)): 18 | self.assertIsInstance(e[i], int) 19 | self.assertEqual(e, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) 20 | 21 | a = [23449054409, 58564582991, 24739686534, 30995859145, 16274600764, 13384701522, 45782350364, 10685194276, 18864211511, 9594013152, 50215903866, 7952180124, 42094717093, 50866816333, 44318421949, 31143511315] 22 | s = 42313265920 23 | e = low_density.attack(a, s) 24 | for i in range(len(a)): 25 | self.assertIsInstance(e[i], int) 26 | self.assertEqual(e, [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]) 27 | -------------------------------------------------------------------------------- /test/test_lwe.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from random import choice 4 | from random import choices 5 | from random import randrange 6 | from unittest import TestCase 7 | 8 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 9 | if sys.path[1] != path: 10 | sys.path.insert(1, path) 11 | 12 | from attacks.lwe import arora_ge 13 | 14 | 15 | class TestLWE(TestCase): 16 | def _generate_samples(self, q, m, n, E, s): 17 | A = [] 18 | b = [] 19 | for i in range(m): 20 | e = choice(E) 21 | A.append([randrange(0, q) for _ in range(n)]) 22 | b.append(e) 23 | for j in range(n): 24 | b[i] = (b[i] + A[i][j] * s[j]) % q 25 | return A, b 26 | 27 | def test_arora_ge(self): 28 | q = 65537 29 | m = 200 30 | n = 10 31 | E = list(range(-1, 2)) 32 | S = list(range(q)) 33 | s = choices(S, k=n) 34 | A, b = self._generate_samples(q, m, n, E, s) 35 | s_ = arora_ge.attack(q, A, b, E) 36 | for i in range(n): 37 | self.assertIsInstance(s_[i], int) 38 | self.assertEqual(s[i], s_[i]) 39 | 40 | m = 10 41 | n = 10 42 | E = list(range(-1, 2)) 43 | S = list(range(2)) 44 | s = choices(S, k=n) 45 | A, b = self._generate_samples(q, m, n, E, s) 46 | s_ = arora_ge.attack(q, A, b, E, S) 47 | for i in range(n): 48 | self.assertIsInstance(s_[i], int) 49 | self.assertEqual(s[i], s_[i]) 50 | 51 | m = 150 52 | n = 10 53 | E = list(range(-2, 3)) 54 | S = list(range(2)) 55 | s = choices(S, k=n) 56 | A, b = self._generate_samples(q, m, n, E, s) 57 | s_ = arora_ge.attack(q, A, b, E, S) 58 | for i in range(n): 59 | self.assertIsInstance(s_[i], int) 60 | self.assertEqual(s[i], s_[i]) 61 | -------------------------------------------------------------------------------- /test/test_mersenne_twister.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import sys 4 | from unittest import TestCase 5 | 6 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 7 | if sys.path[1] != path: 8 | sys.path.insert(1, path) 9 | 10 | from attacks import mersenne_twister 11 | from attacks.mersenne_twister import state_recovery 12 | 13 | 14 | class TestMersenneTwister(TestCase): 15 | def test_state_recovery(self): 16 | mt = mersenne_twister.mt19937() 17 | mt.seed(1812433253, 0) 18 | y = [next(mt) for _ in range(mt.n)] 19 | mt_ = state_recovery.attack_mt19937(y) 20 | self.assertEqual(mt.mt, mt_.mt) 21 | self.assertEqual(mt.index, mt_.index) 22 | for i in range(mt.n): 23 | self.assertEqual(next(mt), next(mt_)) 24 | 25 | mt = mersenne_twister.mt19937() 26 | mt.seed(1812433253, 1234567) 27 | y = [next(mt) for _ in range(mt.n)] 28 | mt_ = state_recovery.attack_mt19937(y) 29 | self.assertEqual(mt.mt, mt_.mt) 30 | self.assertEqual(mt.index, mt_.index) 31 | for i in range(mt.n): 32 | self.assertEqual(next(mt), next(mt_)) 33 | 34 | random.seed(1234567) 35 | y = [random.getrandbits(32) for _ in range(624)] 36 | mt_ = state_recovery.attack_mt19937(y) 37 | for i in range(624): 38 | self.assertEqual(random.getrandbits(32), next(mt_)) 39 | 40 | mt = mersenne_twister.mt19937_64() 41 | mt.seed(6364136223846793005, 0) 42 | y = [next(mt) for _ in range(mt.n)] 43 | mt_ = state_recovery.attack_mt19937_64(y) 44 | self.assertEqual(mt.mt, mt_.mt) 45 | self.assertEqual(mt.index, mt_.index) 46 | for i in range(mt.n): 47 | self.assertEqual(next(mt), next(mt_)) 48 | 49 | mt = mersenne_twister.mt19937_64() 50 | mt.seed(6364136223846793005, 1234567) 51 | y = [next(mt) for _ in range(mt.n)] 52 | mt_ = state_recovery.attack_mt19937_64(y) 53 | self.assertEqual(mt.mt, mt_.mt) 54 | self.assertEqual(mt.index, mt_.index) 55 | for i in range(mt.n): 56 | self.assertEqual(next(mt), next(mt_)) 57 | -------------------------------------------------------------------------------- /test/test_otp.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from math import log10 4 | from unittest import TestCase 5 | 6 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 7 | if sys.path[1] != path: 8 | sys.path.insert(1, path) 9 | 10 | from attacks.otp import key_reuse 11 | 12 | 13 | class TestOTP(TestCase): 14 | def test_key_reuse(self): 15 | # Source: http://pi.math.cornell.edu/~mec/2003-2004/cryptography/subs/frequencies.html 16 | char_frequencies = {"a": 8.12, "b": 1.49, "c": 2.71, "d": 4.32, "e": 12.02, "f": 2.30, "g": 2.03, "h": 5.92, "i": 7.31, "j": 0.10, "k": 0.69, "l": 3.98, "m": 2.61, "n": 6.95, "o": 7.68, "p": 1.82, "q": 0.11, "r": 6.02, "s": 6.28, "t": 9.10, "u": 2.88, "v": 1.11, "w": 2.09, "x": 0.17, "y": 2.11, "z": 0.07} 17 | char_floor = log10(0.01 / 182303) 18 | 19 | key = bytes.fromhex("4d4e2acd3248a5b5ecb106cff94cf10623979966f49a7fd020e395f31c58f0151b9fc78b24b5cf205efee937a8") 20 | lines = [ 21 | b"I used to rule the world\n", 22 | b"Seas would rise when I gave the word\n", 23 | b"Now in the morning, I sleep alone\n", 24 | b"Sweep the streets I used to own\n", 25 | b"I used to roll the dice\n", 26 | b"Feel the fear in my enemy's eyes\n", 27 | b"Listened as the crowd would sing\n", 28 | b"Now the old king is dead, long live the king\n", 29 | b"One minute I held the key\n", 30 | b"Next, the walls were closed on me\n", 31 | b"And I discovered that my castles stand\n", 32 | b"Upon pillars of salt and pillars of sand\n", 33 | b"I hear Jerusalem bells a-ringing\n", 34 | b"Roman cavalry choirs are singing\n", 35 | b"Be my mirror, my sword and shield\n", 36 | b"My missionaries in a foreign field\n", 37 | b"For some reason, I can't explain\n", 38 | b"Once you'd gone, there was never\n", 39 | b"Never an honest word\n", 40 | b"And that was when I ruled the world\n", 41 | b"It was a wicked and wild wind\n", 42 | b"Blew down the doors to let me in\n", 43 | b"Shattered windows and the sound of drums\n", 44 | b"People couldn't believe what I'd become\n", 45 | b"Revolutionaries wait\n", 46 | b"For my head on a silver plate\n", 47 | b"Just a puppet on a lonely string (Mmm, mmm)\n", 48 | b"Oh, who would ever want to be king?\n", 49 | b"I hear Jerusalem bells a-ringing\n", 50 | b"Roman cavalry choirs are singing\n", 51 | b"Be my mirror, my sword and shield\n", 52 | b"My missionaries in a foreign field\n", 53 | b"For some reason, I can't explain\n", 54 | b"I know Saint Peter won't call my name\n", 55 | b"Never an honest word\n", 56 | b"But that was when I ruled the world\n", 57 | ] 58 | 59 | test_known = { 60 | 1: {1: 0, 2: 0, 4: 1, 5: 3}, 61 | 3: {1: 0, 2: 0, 4: 1, 6: 1, 7: 0, 12: 4}, 62 | 6: {1: 0, 2: 0, 4: 0, 6: 0, 7: 0, 12: 0, 16: 3}, 63 | 36: {1: 0, 2: 0, 4: 0, 6: 0, 7: 0, 12: 0, 16: 0, 24: 0, 30: 0, 36: 3, 45: 12}, 64 | } 65 | 66 | for c_size, key_sizes in test_known.items(): 67 | for key_size, diff in key_sizes.items(): 68 | c = [bytes([b ^ key[i % key_size] for i, b in enumerate(line)]) for line in lines[:c_size]] 69 | key_ = key_reuse.attack(c, char_frequencies, char_floor, key_size=key_size) 70 | self.assertEqual(key_size, len(key_)) 71 | self.assertEqual(diff, sum(x != y for x, y in zip(key, key_))) 72 | 73 | test_unknown = { 74 | 1: {5: 3}, 75 | 3: {7: 0, 12: 4}, 76 | 6: {7: 0, 12: 0, 16: 3}, 77 | 36: {4: 0, 6: 0, 7: 0, 12: 0, 16: 0, 24: 0, 30: 0, 36: 3}, 78 | } 79 | 80 | for c_size, key_sizes in test_unknown.items(): 81 | for key_size, diff in key_sizes.items(): 82 | c = [bytes([b ^ key[i % key_size] for i, b in enumerate(line)]) for line in lines[:c_size]] 83 | key_ = key_reuse.attack(c, char_frequencies, char_floor) 84 | self.assertEqual(key_size, len(key_)) 85 | self.assertEqual(diff, sum(x != y for x, y in zip(key, key_))) 86 | -------------------------------------------------------------------------------- /test/test_pseudoprimes.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from unittest import TestCase 4 | 5 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 6 | if sys.path[1] != path: 7 | sys.path.insert(1, path) 8 | 9 | from attacks.pseudoprimes import miller_rabin 10 | 11 | 12 | class TestPseudoprimes(TestCase): 13 | def _miller_rabin(self, n, bases): 14 | assert n > 3 15 | r = 0 16 | d = n - 1 17 | while d % 2 == 0: 18 | r += 1 19 | d //= 2 20 | 21 | for a in bases: 22 | x = pow(a, d, n) 23 | if x == 1 or x == n - 1: 24 | continue 25 | for _ in range(r - 1): 26 | x = pow(x, 2, n) 27 | if x == n - 1: 28 | break 29 | else: 30 | return False 31 | return True 32 | 33 | def test_miller_rabin(self): 34 | bases = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31] 35 | n, p1, p2, p3 = miller_rabin.generate_pseudoprime(bases, min_bit_length=400) 36 | self.assertIsInstance(n, int) 37 | self.assertIsInstance(p1, int) 38 | self.assertIsInstance(p2, int) 39 | self.assertIsInstance(p3, int) 40 | self.assertGreaterEqual(n.bit_length(), 400) 41 | self.assertEqual(n, p1 * p2 * p3) 42 | self.assertTrue(self._miller_rabin(n, bases)) 43 | 44 | bases = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61] 45 | n, p1, p2, p3 = miller_rabin.generate_pseudoprime(bases, min_bit_length=600) 46 | self.assertIsInstance(n, int) 47 | self.assertIsInstance(p1, int) 48 | self.assertIsInstance(p2, int) 49 | self.assertIsInstance(p3, int) 50 | self.assertGreaterEqual(n.bit_length(), 600) 51 | self.assertEqual(n, p1 * p2 * p3) 52 | self.assertTrue(self._miller_rabin(n, bases)) 53 | -------------------------------------------------------------------------------- /test/test_rc4.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from unittest import TestCase 4 | 5 | from Crypto.Cipher import ARC4 6 | 7 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 8 | if sys.path[1] != path: 9 | sys.path.insert(1, path) 10 | 11 | from attacks.rc4 import fms 12 | 13 | 14 | class TestRC4(TestCase): 15 | def _encrypt(self, iv, key, p): 16 | return ARC4.new(iv + key).encrypt(p) 17 | 18 | def test_fms(self): 19 | keys = [ 20 | "3718f3809a", 21 | "7e2160f62e99", 22 | "be0718252b4fbc", 23 | "6463c171f7391d30", 24 | "36a05ac48cd2900621", 25 | "6a72ef3705600df0e025", 26 | "47bf390c9573cf4b5d4c18", 27 | "84be086db8ba1306f6f4b302", 28 | "08e3ac0eb6a483095f92ad6ed1", 29 | "6c13bde5d8cc704045727cf54d36", 30 | "9a1c69c725ddc2d6d5bf0b7853393b", 31 | "70509f20bd38c202eecbbcec070161dd", 32 | "3558cf4e33164375994664286941b5aae7", 33 | "4b37460ddb71f90dfc4b26a9e816ad49beda", 34 | "ca78c64d55e0a2add6625474f7334123a0b59b", 35 | "8ff7efb0a9034d227890e87b1baf07ec3021e797", 36 | "3a7b8d4facc69e982a5c70d179e15a75087a9add53", 37 | "820297475275fca9d8d07939e2ddd76b508432f140b4", 38 | "b2b3e47e81276906491c30d42f9a9ae7daee633d6a2464", 39 | "1eba58bbb83e4f48d3395d9ddf50b50bd797fb230877b0b1", 40 | "1b0339415ef65082bd2040167ab4320c7e11dc1493854faa39", 41 | "966c7fd547317db5a11e1a6cd4b7e17ca36dc942fe961888c381", 42 | "ac87f9eae75e978e6c097b31423e2c522e4232b7f0a3f58db407f1", 43 | ] 44 | 45 | for key in keys: 46 | key = bytearray.fromhex(key) 47 | key_ = fms.attack(lambda iv, p: self._encrypt(iv, key, p), len(key)) 48 | self.assertEqual(key, key_) 49 | -------------------------------------------------------------------------------- /test/test_shamir_secret_sharing.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from hashlib import sha256 4 | from random import randrange 5 | from unittest import TestCase 6 | 7 | from sage.all import GF 8 | 9 | path = os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))) 10 | if sys.path[1] != path: 11 | sys.path.insert(1, path) 12 | 13 | from attacks.shamir_secret_sharing import deterministic_coefficients 14 | from attacks.shamir_secret_sharing import share_forgery 15 | 16 | 17 | class TestShamirSecretSharing(TestCase): 18 | def _eval(self, p, a, x): 19 | y = 0 20 | for i, ai in enumerate(a): 21 | y += ai * x ** i 22 | return y % p 23 | 24 | def test_deterministic_coefficients(self): 25 | p = 3615438361 26 | k = 15 27 | n = 20 28 | s = randrange(1, p) 29 | f = lambda ai: int.from_bytes(sha256(ai.to_bytes(32, byteorder="big")).digest(), byteorder="big") 30 | 31 | a = [s] 32 | for i in range(1, n + 1): 33 | a.append(f(a[i - 1])) 34 | a = a[:k] 35 | 36 | xs = [] 37 | ys = [] 38 | for i in range(n): 39 | x = randrange(1, p) 40 | xs.append(x) 41 | y = self._eval(p, a, x) 42 | ys.append(y) 43 | 44 | s_ = deterministic_coefficients.attack(p, k, a[1], f, xs[0], ys[0]) 45 | self.assertIsInstance(s_, int) 46 | self.assertEqual(s_, s) 47 | 48 | def test_share_forgery(self): 49 | p = 4224273359 50 | k = 15 51 | n = 20 52 | s = randrange(1, p) 53 | s_ = randrange(1, p) 54 | 55 | a = [s] 56 | for i in range(1, n + 1): 57 | a.append(randrange(1, p)) 58 | a = a[:k] 59 | 60 | xs = [] 61 | ys = [] 62 | for i in range(n): 63 | x = randrange(1, p) 64 | xs.append(x) 65 | y = self._eval(p, a, x) 66 | ys.append(y) 67 | 68 | ys[0] = share_forgery.attack(p, s, s_, xs[0], ys[0], xs[1:]) 69 | self.assertIsInstance(ys[0], int) 70 | self.assertEqual(s_, GF(p)["x"].lagrange_polynomial(zip(xs, ys)).constant_coefficient()) 71 | --------------------------------------------------------------------------------