From c99686e3cae709805db1f803345ff4be9c2e3e3f Mon Sep 17 00:00:00 2001 From: Joachim Vandersmissen Date: Fri, 19 Jun 2020 22:12:59 +0200 Subject: [PATCH] Initial batch of attacks --- README.md | 77 ++++++++++++++++ cbc/bit_flipping.py | 19 ++++ cbc/iv_recovery.py | 22 +++++ cbc/padding_oracle.py | 66 +++++++++++++ cbc_and_cbc_mac/eam_key_reuse.py | 40 ++++++++ cbc_and_cbc_mac/etm_key_reuse.py | 41 +++++++++ cbc_and_cbc_mac/mte_key_reuse.py | 43 +++++++++ cbc_mac/length_extension.py | 9 ++ ctr/crime.py | 35 +++++++ ecb/plaintext_recovery.py | 47 ++++++++++ elgamal_encryption/nonce_reuse.py | 12 +++ elgamal_signature/nonce_reuse.py | 36 ++++++++ factorization/coppersmith.py | 69 ++++++++++++++ factorization/fermat.py | 21 +++++ factorization/pollard_rho.py | 23 +++++ factorization/roca.py | 78 ++++++++++++++++ factorization/twin_primes.py | 10 ++ gcm/forbidden_attack.py | 89 ++++++++++++++++++ ofb/crime.py | 34 +++++++ pseudoprimes/miller_rabin.py | 84 +++++++++++++++++ rsa/bleichenbacher_signature_forgery.py | 17 ++++ rsa/boneh_durfee.py | 34 +++++++ rsa/common_modulus.py | 17 ++++ rsa/common_prime_factor.py | 20 ++++ rsa/crt_fault_attack.py | 25 +++++ rsa/extended_wiener_attack.py | 59 ++++++++++++ rsa/hastad_attack.py | 27 ++++++ rsa/low_exponent.py | 11 +++ rsa/partial_key_exposure.py | 44 +++++++++ rsa/related_message.py | 28 ++++++ rsa/stereotyped_message.py | 33 +++++++ rsa/wiener_attack.py | 34 +++++++ .../deterministic_coefficients.py | 18 ++++ shamir_secret_sharing/share_forgery.py | 16 ++++ small_roots/boneh_durfee.py | 76 +++++++++++++++ small_roots/coron.py | 92 +++++++++++++++++++ small_roots/howgrave_graham.py | 51 ++++++++++ 37 files changed, 1457 insertions(+) create mode 100644 README.md create mode 100644 cbc/bit_flipping.py create mode 100644 cbc/iv_recovery.py create mode 100644 cbc/padding_oracle.py create mode 100644 cbc_and_cbc_mac/eam_key_reuse.py create mode 100644 cbc_and_cbc_mac/etm_key_reuse.py create mode 100644 cbc_and_cbc_mac/mte_key_reuse.py create mode 100644 cbc_mac/length_extension.py create mode 100644 ctr/crime.py create mode 100644 ecb/plaintext_recovery.py create mode 100644 elgamal_encryption/nonce_reuse.py create mode 100644 elgamal_signature/nonce_reuse.py create mode 100644 factorization/coppersmith.py create mode 100644 factorization/fermat.py create mode 100644 factorization/pollard_rho.py create mode 100644 factorization/roca.py create mode 100644 factorization/twin_primes.py create mode 100644 gcm/forbidden_attack.py create mode 100644 ofb/crime.py create mode 100644 pseudoprimes/miller_rabin.py create mode 100644 rsa/bleichenbacher_signature_forgery.py create mode 100644 rsa/boneh_durfee.py create mode 100644 rsa/common_modulus.py create mode 100644 rsa/common_prime_factor.py create mode 100644 rsa/crt_fault_attack.py create mode 100644 rsa/extended_wiener_attack.py create mode 100644 rsa/hastad_attack.py create mode 100644 rsa/low_exponent.py create mode 100644 rsa/partial_key_exposure.py create mode 100644 rsa/related_message.py create mode 100644 rsa/stereotyped_message.py create mode 100644 rsa/wiener_attack.py create mode 100644 shamir_secret_sharing/deterministic_coefficients.py create mode 100644 shamir_secret_sharing/share_forgery.py create mode 100644 small_roots/boneh_durfee.py create mode 100644 small_roots/coron.py create mode 100644 small_roots/howgrave_graham.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..618858c --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Crypto attacks +Python implementations of cryptographic attacks and utilities. + +## Requirements +* PyCryptodome +* SageMath + +## Implementations +### CBC +* [x] [Bit flipping attack](cbc/bit_flipping.py) +* [x] [IV recovery attack](cbc/iv_recovery.py) +* [x] [Padding oracle attack](cbc/padding_oracle.py) + +### CBC + CBC-MAC +* [x] [Key reuse attack (encrypt-and-MAC)](cbc_and_cbc_mac/eam_key_reuse.py) +* [x] [Key reuse attack (encrypt-then-MAC)](cbc_and_cbc_mac/eam_key_reuse.py) +* [x] [Key reuse attack (MAC-then-encrypt)](cbc_and_cbc_mac/eam_key_reuse.py) + +### CBC-MAC +* [x] [Length extension attack](cbc_mac/length_extension.py) + +### CTR +* [x] [CRIME attack](ctr/crime.py) + +### ECB +* [x] [Plaintext recovery attack](ecb/plaintext_recovery.py) + +### ElGamal Encryption +* [x] [Nonce reuse attack](elgamal_encryption/nonce_reuse.py) + +### ElgGamal Signature +* [ ] Bleichenbacher's attack +* [ ] Khadir's attack +* [x] [Nonce reuse attack](elgamal_signature/nonce_reuse.py) + +### Factorization +* [x] [Coppersmith factorization](factorization/coppersmith.py) +* [x] [Fermat factorization](factorization/fermat.py) +* [x] [Pollard's Rho factorization](factorization/pollard_rho.py) +* [x] [ROCA](factorization/roca.py) [More information: Nemec M. et al., "The Return of Coppersmith’s A‚ttack: Practical Factorization of Widely Used RSA Moduli"] +* [x] [Twin primes factorization](factorization/twin_primes.py) + +### GCM +* [x] [Forbidden attack](gcm/forbidden_attack.py) [More information: Joux A., "Authentication Failures in NIST version of GCM"] + +##### OFB +* [x] [CRIME attack](ofb/crime.py) + +### Pseudoprimes +* [x] [Generating Miller-Rabin pseudoprimes](pseudoprimes/miller_rabin.py) + +### RSA +* [ ] Bleichenbacher's CCA attack +* [x] [Bleichenbacher's signature forgery attack](rsa/bleichenbacher_signature_forgery.py) +* [x] [Boneh-Durfee attack](rsa/boneh_durfee.py) [More information: Boneh D., Durfee G., "Cryptanalysis of RSA with Private Key d Less than N^0.292"] +* [x] [Common modulus attack](rsa/common_modulus.py) +* [x] [Common prime factor attack](rsa/common_prime_factor.py) +* [x] [CRT fault attack](rsa/crt_fault_attack.py) +* [x] [Extended Wiener's attack](rsa/extended_wiener_attack.py) [More information: Dujella A., "Continued fractions and RSA with small secret exponent"] +* [x] [Hastad's broadcast attack](rsa/hastad_attack.py) +* [x] [Low public exponent attack](rsa/low_exponent.py) +* [ ] LSB oracle attack +* [ ] Manger's attack +* [x] [Partial key exposure attack for low public exponents](rsa/partial_key_exposure.py) [More information: Boneh D., Durfee G., Frankel Y., "An Attack on RSA Given a Small Fraction of the Private Key Bits"] +* [x] [Related message attack](rsa/related_message.py) +* [x] [Stereotyped message attack](rsa/stereotyped_message.py) +* [x] [Wiener's attack](rsa/wiener_attack.py) + +### Shamir's Secret Sharing +* [x] [Deterministic coefficients](shamir_secret_sharing/deterministic_coefficients.py) +* [x] [Share forgery](shamir_secret_sharing/share_forgery.py) + +### Small roots +* [x] [Boneh-Durfee method](small_roots/boneh_durfee.py) [More information: Boneh D., Durfee G., "Cryptanalysis of RSA with Private Key d Less than N^0.292"] +* [x] [Coron method](small_roots/coron.py) [More information: Coron J., "Finding Small Roots of Bivariate Integer Polynomial Equations: a Direct Approach"] +* [x] [Howgrave-Graham method](small_roots/howgrave_graham.py) [More information: May A., "New RSA Vulnerabilities Using Lattice Reduction Methods"] +* [ ] Jochemsz-May method [More information: Jochemsz E., May A., "A Strategy for Finding Roots of Multivariate Polynomials with New Applications in Attacking RSA Variants"] diff --git a/cbc/bit_flipping.py b/cbc/bit_flipping.py new file mode 100644 index 0000000..27e027c --- /dev/null +++ b/cbc/bit_flipping.py @@ -0,0 +1,19 @@ +def attack(iv, c, pos, p, p_): + """ + Replaces the original plaintext with a new plaintext at a position in the ciphertext + :param iv: the initialization vector + :param c: the ciphertext + :param pos: the position to modify at + :param p: the original plaintext + :param p_: the new plaintext + :return: a tuple containing the modified initialization vector and the modified ciphertext + """ + iv_ = bytearray(iv) + c_ = bytearray(c) + for i in range(len(p)): + if pos + i < 16: + iv_[pos + i] = iv[pos + i] ^ p[i] ^ p_[i] + else: + c_[pos + i - 16] = c[pos + i - 16] ^ p[i] ^ p_[i] + + return bytes(iv_), bytes(c_) diff --git a/cbc/iv_recovery.py b/cbc/iv_recovery.py new file mode 100644 index 0000000..2999568 --- /dev/null +++ b/cbc/iv_recovery.py @@ -0,0 +1,22 @@ +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +from Crypto.Util.strxor import strxor + +key = get_random_bytes(16) + + +def _encrypt(p): + return AES.new(key, AES.MODE_CBC, key).encrypt(p) + + +def _decrypt(c): + return AES.new(key, AES.MODE_CBC, key).decrypt(c) + + +def attack(): + """ + Recovers the initialization vector using a chosen-ciphertext attack. + :return: the initialization vector + """ + p = _decrypt(bytes(32)) + return strxor(p[:16], p[16:]) diff --git a/cbc/padding_oracle.py b/cbc/padding_oracle.py new file mode 100644 index 0000000..8e56194 --- /dev/null +++ b/cbc/padding_oracle.py @@ -0,0 +1,66 @@ +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +from Crypto.Util.Padding import pad +from Crypto.Util.Padding import unpad + +key = get_random_bytes(16) + + +def _encrypt(p): + iv = get_random_bytes(16) + return iv, AES.new(key, AES.MODE_CBC, iv).encrypt(pad(p, 16)) + + +def _valid_padding(iv, c): + try: + unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(c), 16) + return True + except ValueError: + return False + + +def _correct_padding(iv, c, i): + if not _valid_padding(iv, c): + return False + + # Special handling for last byte of last block + if i == 15: + iv[14] ^= 1 + return _valid_padding(iv, c) + + return True + + +def _attack_block(iv, c): + dc = bytearray(16) + p = bytearray(16) + iv_ = bytearray(iv) + for i in reversed(range(16)): + # The padding byte for this position. + pb = 16 - i + # Apply padding byte to iv. + for j in reversed(range(i + 1, 16)): + iv_[j] = dc[j] ^ pb + + # Try every byte until padding is correct. + for b in range(256): + iv_[i] = b + if _correct_padding(iv_, c, i): + dc[i] = b ^ pb + p[i] = dc[i] ^ iv[i] + + return p + + +def attack(iv, c): + """ + Recovers the plaintext using the padding oracle attack. + :param iv: the initialization vector + :param c: the ciphertext + :return: the plaintext + """ + p = _attack_block(iv, c) + for i in range(16, len(c), 16): + p += _attack_block(c[i - 16:i], c[i:i + 16]) + + return unpad(p, 16) diff --git a/cbc_and_cbc_mac/eam_key_reuse.py b/cbc_and_cbc_mac/eam_key_reuse.py new file mode 100644 index 0000000..5ce635b --- /dev/null +++ b/cbc_and_cbc_mac/eam_key_reuse.py @@ -0,0 +1,40 @@ +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +from Crypto.Util.Padding import pad +from Crypto.Util.Padding import unpad + +zero_iv = bytes(16) +key = get_random_bytes(16) + + +# Notice how the key is used for encryption and authentication... +def _encrypt(p): + p = pad(p, 16) + iv = get_random_bytes(16) + c = AES.new(key, AES.MODE_CBC, iv).encrypt(p) + # Encrypt-and-MAC using CBC-MAC to prevent chosen-ciphertext attacks. + t = AES.new(key, AES.MODE_CBC, zero_iv).encrypt(p)[-16:] + return iv, c, t + + +def _decrypt(iv, c, t): + p = AES.new(key, AES.MODE_CBC, iv).decrypt(c) + t_ = AES.new(key, AES.MODE_CBC, zero_iv).encrypt(p)[-16:] + # Check the MAC to be sure the message isn't forged. + if t != t_: + return None + + return unpad(p, 16) + + +def attack(iv, c, t): + """ + Uses a chosen-ciphertext attack to decrypt the ciphertext. + :param iv: the initialization vector + :param c: the ciphertext + :param t: the tag corresponding to the ciphertext + :return: the plaintext + """ + c_ = iv + c + p_ = _decrypt(bytes(16), c_, c[-16:]) + return p_[16:] diff --git a/cbc_and_cbc_mac/etm_key_reuse.py b/cbc_and_cbc_mac/etm_key_reuse.py new file mode 100644 index 0000000..e96938d --- /dev/null +++ b/cbc_and_cbc_mac/etm_key_reuse.py @@ -0,0 +1,41 @@ +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +from Crypto.Util.Padding import pad +from Crypto.Util.Padding import unpad + +zero_iv = bytes(16) +key = get_random_bytes(16) + + +# Notice how the key is used for encryption and authentication... +def _encrypt(p): + p = pad(p, 16) + iv = get_random_bytes(16) + c = AES.new(key, AES.MODE_CBC, iv).encrypt(p) + # Encrypt-then-MAC using CBC-MAC to prevent chosen-ciphertext attacks. + t = AES.new(key, AES.MODE_CBC, zero_iv).encrypt(iv + c)[-16:] + return iv, c, t + + +def _decrypt(iv, c, t): + t_ = AES.new(key, AES.MODE_CBC, zero_iv).encrypt(iv + c)[-16:] + # Check the MAC to be sure the message isn't forged. + if t != t_: + return None + + return unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(c), 16) + + +def attack(iv, c, t): + """ + Uses a chosen-ciphertext attack to decrypt the ciphertext. + :param iv: the initialization vector + :param c: the ciphertext + :param t: the tag corresponding to the ciphertext + :return: the plaintext + """ + p_ = bytes(16) + iv + c + iv_, c_, t_ = _encrypt(p_) + c__ = iv + c + p__ = _decrypt(iv_, c__, c_[-32:-16]) + return p__[16:] diff --git a/cbc_and_cbc_mac/mte_key_reuse.py b/cbc_and_cbc_mac/mte_key_reuse.py new file mode 100644 index 0000000..a24ea4e --- /dev/null +++ b/cbc_and_cbc_mac/mte_key_reuse.py @@ -0,0 +1,43 @@ +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +from Crypto.Util.Padding import pad +from Crypto.Util.Padding import unpad + +zero_iv = bytes(16) +key = get_random_bytes(16) + + +# Notice how the key is used for encryption and authentication... +def _encrypt(p): + p = pad(p, 16) + iv = get_random_bytes(16) + # MAC-then-encrypt using CBC-MAC to prevent chosen-ciphertext attacks. + t = AES.new(key, AES.MODE_CBC, zero_iv).encrypt(p)[-16:] + c = AES.new(key, AES.MODE_CBC, iv).encrypt(p + t) + return iv, c + + +def _decrypt(iv, c): + d = AES.new(key, AES.MODE_CBC, iv).decrypt(c) + p = d[:-16] + t = d[-16:] + t_ = AES.new(key, AES.MODE_CBC, zero_iv).encrypt(p)[-16:] + # Check the MAC to be sure the message isn't forged. + if t != t_: + return None + + return unpad(p, 16) + + +def attack(iv, c, encrypted_zeroes): + """ + Uses a chosen-ciphertext attack to decrypt the ciphertext. + Prior knowledge of E_k(0^16) is required for this attack to work. + :param iv: the initialization vector + :param c: the ciphertext + :param encrypted_zeroes: a full zero block encrypted using the key + :return: the plaintext + """ + c_ = iv + c[:-16] + encrypted_zeroes + p_ = _decrypt(bytes(16), c_) + return p_[16:] diff --git a/cbc_mac/length_extension.py b/cbc_mac/length_extension.py new file mode 100644 index 0000000..53d7c92 --- /dev/null +++ b/cbc_mac/length_extension.py @@ -0,0 +1,9 @@ +from Crypto.Util.strxor import strxor + + +def attack(m1, t1, m2, t2): + m3 = bytearray(m1) + m3 += strxor(t1, m2[:16]) + for i in range(16, len(m2), 16): + m3 += m2[i:i + 16] + return m3, t2 diff --git a/ctr/crime.py b/ctr/crime.py new file mode 100644 index 0000000..b4f80ef --- /dev/null +++ b/ctr/crime.py @@ -0,0 +1,35 @@ +import zlib + +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +from Crypto.Util import Counter + +key = get_random_bytes(16) +secret = get_random_bytes(16) + + +def _encrypt(p): + return AES.new(key, AES.MODE_CTR, counter=Counter.new(128)).encrypt(zlib.compress(p + secret)) + + +def attack(secret_len): + """ + Recovers a secret using the CRIME attack (CTR version). + :param secret_len: the length of the secret to recover + :return: the secret + """ + padding = bytearray() + for i in range(secret_len): + padding.append(i) + + s = bytearray() + for i in range(secret_len): + min = None + for j in range(256): + l = len(_encrypt(padding + s + bytes([j]) + padding)) + if min is None or l < min[0]: + min = (l, j) + + s.append(min[1]) + + return bytes(s) diff --git a/ecb/plaintext_recovery.py b/ecb/plaintext_recovery.py new file mode 100644 index 0000000..c4b8aae --- /dev/null +++ b/ecb/plaintext_recovery.py @@ -0,0 +1,47 @@ +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +from Crypto.Util.Padding import pad + +key = get_random_bytes(16) +secret = get_random_bytes(16) + + +def _encrypt(p): + return AES.new(key, AES.MODE_ECB).encrypt(pad(p + secret, 16)) + + +def _calculate_length(): + p = bytearray() + c = _encrypt(p) + l = len(c) + while len(c) == l: + p.append(0) + c = _encrypt(p) + + return l - len(p) + + +def attack(): + """ + Recovers a secret which is appended to a plaintext and encrypted using ECB. + :return: the secret + """ + # Calculate the length of the secret. + l = _calculate_length() + s = bytearray() + # Make sure the plaintext ends with a single character in a block. + extra = bytearray((17 - l) % 16) + for i in range(l): + s.insert(0, 0) + # Try every btyte. + for j in range(256): + s[0] = j + padded = pad(s, 16) + c = _encrypt(padded + extra) + # The active p block equals the active s block. + if c[len(c) - len(padded):] == c[:len(padded)]: + break + + extra.append(0) + + return bytes(s) diff --git a/elgamal_encryption/nonce_reuse.py b/elgamal_encryption/nonce_reuse.py new file mode 100644 index 0000000..58d353d --- /dev/null +++ b/elgamal_encryption/nonce_reuse.py @@ -0,0 +1,12 @@ +def attack(p, m1, c1, d1, c2, d2): + """ + Recovers a secret plaintext encrypted using the same nonce as a previous, known plaintext. + :param p: the prime used in the ElGamal scheme + :param m1: the known plaintext + :param c1: the ciphertext of the known plaintext + :param d1: the ciphertext of the known plaintext + :param c2: the ciphertext of the secret plaintext + :param d2: the ciphertext of the secret plaintext + :return: the secret plaintext + """ + return pow(d1, -1, p) * d2 * m1 % p diff --git a/elgamal_signature/nonce_reuse.py b/elgamal_signature/nonce_reuse.py new file mode 100644 index 0000000..d8c2fd4 --- /dev/null +++ b/elgamal_signature/nonce_reuse.py @@ -0,0 +1,36 @@ +from math import gcd + + +# Solves a congruence of the form ax = b mod n +def solve_congruence(a, b, n): + g = gcd(a, n) + a //= g + b //= g + m = n // g + for i in range(g): + yield (pow(a, -1, m) * b + i * m) % n + + +def attack(p, g, y, m1, r1, s1, m2, r2, s2): + """ + Recovers the private key from two messages signed using the same nonce. + :param p: the prime used in the ElGamal scheme + :param g: the generator used in the ElGamal scheme + :param y: the public key + :param m1: the first message + :param r1: the signature of the first message + :param s1: the signature of the first message + :param m2: the second message + :param r2: the signature of the second message + :param s2: the signature of the second message + :return: the private key + """ + for k in solve_congruence(s1 - s2, m1 - m2, p - 1): + if pow(g, k, p) == r1: + break + + for x in solve_congruence(r1, m1 - k * s1, p - 1): + if pow(g, x, p) == y: + return x + + return None diff --git a/factorization/coppersmith.py b/factorization/coppersmith.py new file mode 100644 index 0000000..288e3f8 --- /dev/null +++ b/factorization/coppersmith.py @@ -0,0 +1,69 @@ +import logging + +from sage.all import PolynomialRing +from sage.all import ZZ +from sage.all import Zmod + +from small_roots.coron import integer_bivariate +from small_roots.howgrave_graham import modular_univariate + + +def factorize_univariate(n, bits, msb_known, msb, lsb_known, lsb): + """ + Recovers the prime factors from a modulus using Coppersmith's method. + :param n: the modulus + :param bits: the amount of bits of the target prime factor + :param msb_known: the amount of known most significant bits of the target prime factor + :param msb: the known most significant bits of the target prime factor + :param lsb_known: the amount of known least significant bits of the target prime factor + :param lsb: the known least significant bits of the target prime factor + :return: a tuple containing the prime factors + """ + pr = PolynomialRing(Zmod(n), "x") + x = pr.gen() + f = msb * 2 ** (bits - msb_known) + x * 2 ** lsb_known + lsb + bound = 2 ** (bits - msb_known - lsb_known) + m = 1 + while True: + t = m + logging.debug(f"Trying m = {m}, t = {t}...") + for root in modular_univariate(f, n, m, t, bound): + p = msb * 2 ** (bits - msb_known) + root * 2 ** lsb_known + lsb + if p != 0 and n % p == 0: + return p, n // p + + m += 1 + + +def factorize_bivariate(n, p_bits, p_msb_known, p_msb, p_lsb_known, p_lsb, q_bits, q_msb_known, q_msb, q_lsb_known, q_lsb): + """ + Recovers the prime factors from a modulus using Coppersmith's method. + For more complex combinations of known bits, the coron module in the small_roots package should be used directly. + :param n: the modulus + :param p_bits: the amount of bits of the first prime factor + :param p_msb_known: the amount of known most significant bits of the first prime factor + :param p_msb: the known most significant bits of the first prime factor + :param p_lsb_known: the amount of known least significant bits of the first prime factor + :param p_lsb: the known least significant bits of the first prime factor + :param q_bits: the amount of bits of the second prime factor + :param q_msb_known: the amount of known most significant bits of the second prime factor + :param q_msb: the known most significant bits of the second prime factor + :param q_lsb_known: the amount of known least significant bits of the second prime factor + :param q_lsb: the known least significant bits of the second prime factor + :return: a tuple containing the prime factors + """ + pr = PolynomialRing(ZZ, "x, y") + x, y = pr.gens() + f = (p_msb * 2 ** (p_bits - p_msb_known) + x * 2 ** p_lsb_known + p_lsb) * (q_msb * 2 ** (q_bits - q_msb_known) + y * 2 ** q_lsb_known + q_lsb) - n + xbound = 2 ** (p_bits - p_msb_known - p_lsb_known) + ybound = 2 ** (q_bits - q_msb_known - q_lsb_known) + k = 1 + while True: + logging.debug(f"Trying k = {k}...") + for xroot, yroot in integer_bivariate(f, k, xbound, ybound): + p = p_msb * 2 ** (p_bits - p_msb_known) + xroot * 2 ** p_lsb_known + p_lsb + q = q_msb * 2 ** (q_bits - q_msb_known) + yroot * 2 ** q_lsb_known + q_lsb + if p * q == n: + return p, q + + k += 1 diff --git a/factorization/fermat.py b/factorization/fermat.py new file mode 100644 index 0000000..6b9a853 --- /dev/null +++ b/factorization/fermat.py @@ -0,0 +1,21 @@ +from math import isqrt + + +def factorize(n): + """ + Recovers the prime factors from a modulus using Fermat's factorization method. + :param n: the modulus + :return: a tuple containing the prime factors + """ + a = isqrt(n) + b = a * a - n + while b < 0 or isqrt(b) ** 2 != b: + a += 1 + b = a * a - n + + p = a - isqrt(b) + q = n // p + if p * q == n: + return p, q + + raise ValueError(f"Failed to factorize.") diff --git a/factorization/pollard_rho.py b/factorization/pollard_rho.py new file mode 100644 index 0000000..a7d085c --- /dev/null +++ b/factorization/pollard_rho.py @@ -0,0 +1,23 @@ +from math import gcd + + +def factorize(n, g=lambda x, n: (x ** 2 + 1) % n, start=2): + """ + Recovers the prime factors from a modulus using Pollard's Rho algorithm. + :param n: the modulus + :param g: the polynomial (default: (x ** 2 + 1) % n) + :param start: the starting value (default: 2) + :return: a tuple containing the prime factors + """ + x = start + y = start + d = 1 + while d == 1: + x = g(x, n) + y = g(g(y, n), n) + d = gcd(abs(x - y), n) + + if d == n: + raise ValueError(f"Failed to factorize (starting value: {start}).") + + return d, n // d diff --git a/factorization/roca.py b/factorization/roca.py new file mode 100644 index 0000000..7a0640c --- /dev/null +++ b/factorization/roca.py @@ -0,0 +1,78 @@ +import logging +from math import log2 + +from sage.all import PolynomialRing +from sage.all import Zmod +from sage.all import discrete_log +from sage.all import factor + +from small_roots.howgrave_graham import modular_univariate + + +def _prime_power_divisors(n): + divisors = [] + for p, e in factor(n): + for i in range(1, e + 1): + divisors.append(p ** i) + + divisors.sort() + return divisors + + +def _compute_max_primorial_(primorial, order): + for p in _prime_power_divisors(primorial): + orderp = Zmod(p)(65537).multiplicative_order() + if order % orderp != 0: + primorial //= p + + return primorial + + +def _greedy_find_primorial_(n, primorial): + order = Zmod(primorial)(65537).multiplicative_order() + while True: + best_reward = 0 + best_order = order + best_primorial_ = primorial + for p in _prime_power_divisors(order): + order_ = order // p + primorial_ = _compute_max_primorial_(primorial, order_) + r = (log2(order) - log2(order_)) / (log2(primorial) - log2(primorial_)) + if r > best_reward: + best_reward = r + best_order = order_ + best_primorial_ = primorial_ + + if log2(best_primorial_) < log2(n) / 4: + return primorial + + order = best_order + primorial = best_primorial_ + + +def attack(n, primorial, m): + """ + Recovers the prime factors from a modulus using the ROCA method. + More information: Nemec M. et al., "The Return of Coppersmith’s A‚ttack: Practical Factorization of Widely Used RSA Moduli" + :param n: the modulus + :param primorial: the primorial used to generate the primes + :param m: the m parameter for Coppersmith's method + :return: a tuple containing the prime factors + """ + logging.debug("Generating primorial_...") + primorial_ = _greedy_find_primorial_(n, primorial) + inverse_primorial_ = pow(primorial_, -1, n) + e = Zmod(primorial_)(65537) + c = discrete_log(n, e) + order = e.multiplicative_order() + + logging.debug("Starting exhaustive a search...") + pr = PolynomialRing(Zmod(n), "x") + x = pr.gen() + bound = int(2 * n ** 0.5 // primorial_) + for a in range(c // 2, (c + order) // 2 + 1): + f = x + inverse_primorial_ * int(e ** a) + for root in modular_univariate(f, n, m, m + 1, bound): + p = root * primorial_ + int(e ** a) + if n % p == 0: + return p, n // p diff --git a/factorization/twin_primes.py b/factorization/twin_primes.py new file mode 100644 index 0000000..dc6e246 --- /dev/null +++ b/factorization/twin_primes.py @@ -0,0 +1,10 @@ +from math import isqrt + + +def factorize(n): + """ + Recovers the prime factors from a modulus if the factors are twin primes. + :param n: the modulus + :return: a tuple containing the prime factors + """ + return isqrt(n + 1) - 1, isqrt(n + 1) + 1 diff --git a/gcm/forbidden_attack.py b/gcm/forbidden_attack.py new file mode 100644 index 0000000..c6446ad --- /dev/null +++ b/gcm/forbidden_attack.py @@ -0,0 +1,89 @@ +from Crypto.Util.number import bytes_to_long +from Crypto.Util.number import long_to_bytes +from sage.all import GF + +gf2 = GF(2) +gf2x = gf2.polynomial_ring("x") +x = gf2x.gen() +gf2e = GF(2 ** 128, name="y", modulus=x ** 128 + x ** 7 + x ** 2 + x + 1) + + +# Converts an integer to a gf2e element, little endian. +def _to_gf2e(n): + return gf2e([(n >> i) & 1 for i in range(127, -1, -1)]) + + +# Converts a gfe2 element to an integer, little endian. +def _from_gf2e(p): + n = p.integer_representation() + ans = n & 1 + for i in range(127): + ans <<= 1 + n >>= 1 + ans |= n & 1 + + return ans + + +# Calculates the GHASH polynomial. +def _ghash(h, a, c): + la = len(a) + lc = len(c) + p = gf2e.zero() + for i in range(la // 16): + p += _to_gf2e(bytes_to_long(a[16 * i:16 * (i + 1)])) + p *= h + + if la % 16 != 0: + p += _to_gf2e(bytes_to_long(a[-(la % 16):] + bytes(16 - la % 16))) + p *= h + + for i in range(lc // 16): + p += _to_gf2e(bytes_to_long(c[16 * i:16 * (i + 1)])) + p *= h + + if lc % 16 != 0: + p += _to_gf2e(bytes_to_long(c[-(lc % 16):] + bytes(16 - lc % 16))) + p *= h + + p += _to_gf2e(((8 * la) << 64) | (8 * lc)) + p *= h + return p + + +def recover_possible_auth_keys(a1, c1, t1, a2, c2, t2): + """ + Recovers possible authentication keys from two messages encrypted with the same authentication key. + More information: Joux A., "Authentication Failures in NIST version of GCM" + :param a1: the associated data of the first message (bytes) + :param c1: the ciphertext of the first message (bytes) + :param t1: the authentication tag of the first message (bytes) + :param a2: the associated data of the second message (bytes) + :param c2: the ciphertext of the second message (bytes) + :param t2: the authentication tag of the second message (bytes) + :return: a generator generating possible authentication keys (gf2e element) + """ + h = gf2e.polynomial_ring("h").gen() + p1 = _ghash(h, a1, c1) + _to_gf2e(bytes_to_long(t1)) + p2 = _ghash(h, a2, c2) + _to_gf2e(bytes_to_long(t2)) + p = (p1 + p2).monic() + for root in p.roots(): + yield root[0] + + +def forge_tag(h, a, c, t, target_a, target_c): + """ + Forges an authentication tag for a target message given a message with a known tag. + This method is best used with the authentication keys generated by the recover_possible_auth_keys method. + More information: Joux A., "Authentication Failures in NIST version of GCM" + :param h: the authentication key to use (gf2e element) + :param a: the associated data of the message with the known tag (bytes) + :param c: the ciphertext of the message with the known tag (bytes) + :param t: the known authentication tag (bytes) + :param target_a: the target associated data (bytes) + :param target_c: the target ciphertext (bytes) + :return: the forged authenticatino tag (bytes) + """ + ghash = _from_gf2e(_ghash(h, a, c)) + target_ghash = _from_gf2e(_ghash(h, target_a, target_c)) + return long_to_bytes(ghash ^ bytes_to_long(t) ^ target_ghash) diff --git a/ofb/crime.py b/ofb/crime.py new file mode 100644 index 0000000..b823977 --- /dev/null +++ b/ofb/crime.py @@ -0,0 +1,34 @@ +import zlib + +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes + +key = get_random_bytes(16) +secret = get_random_bytes(16) + + +def _encrypt(p): + return AES.new(key, AES.MODE_OFB).encrypt(zlib.compress(p + secret)) + + +def attack(secret_len): + """ + Recovers a secret using the CRIME attack (OFB version). + :param secret_len: the length of the secret to recover + :return: the secret + """ + padding = bytearray() + for i in range(secret_len): + padding.append(i) + + s = bytearray() + for i in range(secret_len): + min = None + for j in range(256): + l = len(_encrypt(padding + s + bytes([j]) + padding)) + if min is None or l < min[0]: + min = (l, j) + + s.append(min[1]) + + return bytes(s) diff --git a/pseudoprimes/miller_rabin.py b/pseudoprimes/miller_rabin.py new file mode 100644 index 0000000..40a3f75 --- /dev/null +++ b/pseudoprimes/miller_rabin.py @@ -0,0 +1,84 @@ +from sage.all import crt +from sage.all import inverse_mod +from sage.all import is_prime +from sage.all import kronecker +from sage.all import next_prime + + +def _generate_s(bases, k2, k3): + s = [] + for b in bases: + s_b = set() + for p in range(1, 4 * b, 2): + if kronecker(b, p) == -1: + s_b.add(p) + + s.append(s_b) + + for i in range(len(s)): + mod = 4 * bases[i] + inv2 = inverse_mod(k2, mod) + inv3 = inverse_mod(k3, mod) + s2 = set() + s3 = set() + for z in s[i]: + s2.add(inv2 * (z + k2 - 1) % mod) + s3.add(inv3 * (z + k3 - 1) % mod) + + s[i] &= s2 & s3 + + return s + + +# Brute forces a combination of residues from s by backtracking +def _backtrack(s, bases, residues, moduli, i): + if i == len(s): + combined_modulus = 1 + for modulus in moduli: + combined_modulus *= modulus + + return crt(residues, moduli), combined_modulus + + moduli.append(4 * bases[i]) + for residue in s[i]: + residues.append(residue) + try: + crt(residues, moduli) + ans = _backtrack(s, bases, residues, moduli, i + 1) + if ans: + return ans + except ValueError: + pass + residues.pop() + + moduli.pop() + return None, None + + +def generate_pseudoprime(bases, min_bitsize=0): + """ + Generates a pseudoprime which passes the Miller-Rabin primality test for the provided bases. + :param bases: the bases + :param min_bitsize: the minimum bitsize of the generated pseudoprime (default: 0) + :return: a tuple containing the pseudoprime, as well as its 3 prime factors + """ + bases.sort() + k2 = next_prime(bases[-1]) + k3 = next_prime(k2) + while True: + residues = [inverse_mod(-k2, k3), inverse_mod(-k3, k2)] + moduli = [k3, k2] + s = _generate_s(bases, k2, k3) + residue, modulus = _backtrack(s, bases, residues, moduli, 0) + if residue and modulus: + i = (2 ** (min_bitsize // 3)) // modulus + while True: + p1 = residue + i * modulus + p2 = k2 * (p1 - 1) + 1 + p3 = k3 * (p1 - 1) + 1 + if is_prime(p1) and is_prime(p2) and is_prime(p3): + return p1 * p2 * p3, p1, p2, p3 + + i += 1 + else: + k3 = next_prime(k3) diff --git a/rsa/bleichenbacher_signature_forgery.py b/rsa/bleichenbacher_signature_forgery.py new file mode 100644 index 0000000..ca7ade2 --- /dev/null +++ b/rsa/bleichenbacher_signature_forgery.py @@ -0,0 +1,17 @@ +def attack(suffix): + """ + Returns a number s for which s^3 ends with the provided suffix. + :param suffix: the suffix + :return: the number s + """ + s = 1 + c = 1 + i = 0 + while (1 << i) <= suffix: + if ((c >> i) & 1) != ((suffix >> i) & 1): + s ^= (1 << i) + c = s ** 3 + + i += 1 + + return s diff --git a/rsa/boneh_durfee.py b/rsa/boneh_durfee.py new file mode 100644 index 0000000..0f35da4 --- /dev/null +++ b/rsa/boneh_durfee.py @@ -0,0 +1,34 @@ +import logging + +from sage.all import PolynomialRing +from sage.all import RealNumber +from sage.all import Zmod + +from small_roots.boneh_durfee import modular_bivariate + + +def attack(n, e, delta=0.25): + """ + Recovers the private exponent if the private exponent is too small. + More information: Boneh D., Durfee G., "Cryptanalysis of RSA with Private Key d Less than N^0.292" + :param n: the modulus + :param e: the public exponent + :param delta: a predicted bound on the private exponent (d < n^delta) (default: 0.25) + :return: the private exponent + """ + pr = PolynomialRing(Zmod(e), "x, y") + x, y = pr.gens() + a = (n + 1) // 2 + f = x * (a + y) + 1 + xbound = int(e ** RealNumber(delta)) + ybound = int(e ** RealNumber(0.5)) + m = 1 + while True: + t = int(m * (1 - 2 * delta)) + logging.debug(f"Trying m = {m}, t = {t}...") + for xroot, yroot in modular_bivariate(f, e, m, t, xbound, ybound): + z = xroot * (a + yroot) + 1 + if z % e == 0: + return z // e + + m += 1 diff --git a/rsa/common_modulus.py b/rsa/common_modulus.py new file mode 100644 index 0000000..3acd7ee --- /dev/null +++ b/rsa/common_modulus.py @@ -0,0 +1,17 @@ +from sage.all import xgcd + + +def attack(n, e1, c1, e2, c2): + """ + Recovers the plaintext from two ciphertexts, encrypted using the same modulus and different public exponents. + :param n: the common modulus + :param e1: the first public exponent + :param c1: the ciphertext of the first encryption + :param e2: the second public exponent + :param c2: the ciphertext of the second encryption + :return: the plaintext + """ + _, u, v = xgcd(e1, e2) + p1 = pow(c1, u, n) if u > 0 else pow(pow(c1, -1, n), -u, n) + p2 = pow(c2, v, n) if v > 0 else pow(pow(c2, -1, n), -v, n) + return p1 * p2 % n diff --git a/rsa/common_prime_factor.py b/rsa/common_prime_factor.py new file mode 100644 index 0000000..7b21070 --- /dev/null +++ b/rsa/common_prime_factor.py @@ -0,0 +1,20 @@ +from math import gcd + + +def attack(n1, e1, c1, n2, e2, c2): + """ + Recovers the plaintexts from two ciphertexts, encrypted using two moduli which share a prime factor. + :param n1: the first modulus + :param e1: the first public exponent + :param c1: the ciphertext of the first encryption + :param n2: the second modulus + :param e2: the second public exponent + :param c2: the ciphertext of the second encryption + :return: a tuple containing the shared prime factor, the second prime factor of the first modulus, the first plaintext, the second prime factor of the second modulus, the second plaintext + """ + p = gcd(n1, n2) + q1 = n1 // p + q2 = n2 // p + d1 = pow(e1, -1, (p - 1) * (q1 - 1)) + d2 = pow(e2, -1, (p - 1) * (q2 - 1)) + return p, q1, pow(c1, d1, n1), q2, pow(c2, d2, n2) diff --git a/rsa/crt_fault_attack.py b/rsa/crt_fault_attack.py new file mode 100644 index 0000000..d1f2388 --- /dev/null +++ b/rsa/crt_fault_attack.py @@ -0,0 +1,25 @@ +from math import gcd + +from sage.all import crt + + +# An example faulty signing function +def _faulty_sign(m, p, q, d_p, d_q): + s_p = pow(m, d_p, p) + s_q = pow(m, d_q, q) + s_q ^= 1 + return crt([s_p, s_q], [p, q]) + + +def attack(n, e, sign): + """ + Recovers the prime factors from a modulus using a faulty RSA-CRT signing function. + :param n: the modulus + :param e: the public exponent + :param sign: the faulty RSA-CRT signing function + :return: a tuple containing the prime factors + """ + m = 2 + s = sign(m) + g = gcd(m - pow(s, e, n), n) + return g, n // g diff --git a/rsa/extended_wiener_attack.py b/rsa/extended_wiener_attack.py new file mode 100644 index 0000000..b17966d --- /dev/null +++ b/rsa/extended_wiener_attack.py @@ -0,0 +1,59 @@ +from math import isqrt + +from sage.all import Integer +from sage.all import RealNumber +from sage.all import continued_fraction + + +def _solve_quadratic(a, b, c): + d = b ** 2 - 4 * a * c + if d < 0: + return 0, 0 + else: + return (-b + isqrt(d)) // (2 * a), (-b - isqrt(d)) // (2 * a) + + +def attack(n, e, max_s=20000, max_r=100, max_t=100): + """ + Recovers the prime factors of a modulus and the private exponent if the private exponent is too small. + More information: Dujella A., "Continued fractions and RSA with small secret exponent" + :param n: the modulus + :param e: the public exponent + :param max_s: the amount of s values to try (default: 20000) + :param max_r: the amount of r values to try for each s value (default: 100) + :param max_t: the amount of t values to try for each s value (default: 100) + :return: a tuple containing the prime factors of the modulus and the private exponent + """ + i_n = Integer(n) + i_e = Integer(e) + threshold = i_e / i_n + (RealNumber(2.122) * i_e) / (i_n * i_n.sqrt()) + convergents = continued_fraction(i_e / i_n).convergents() + for i in range(1, len(convergents) - 2, 2): + if convergents[i + 2] < threshold < convergents[i]: + m = i + break + + for s in range(max_s): + for r in range(max_r): + k = r * convergents[m + 1].numerator() + s * convergents[m + 1].numerator() + d = r * convergents[m + 1].denominator() + s * convergents[m + 1].denominator() + if k == 0 or (e * d - 1) % k != 0: + continue + + phi = (e * d - 1) // k + p, q = _solve_quadratic(1, -n + phi - 1, n) + if p * q == n: + return p, q, d + + for t in range(max_t): + k = s * convergents[m + 2].numerator() - t * convergents[m + 1].numerator() + d = s * convergents[m + 2].denominator() - t * convergents[m + 1].denominator() + if k == 0 or (e * d - 1) % k != 0: + continue + + phi = (e * d - 1) // k + p, q = _solve_quadratic(1, -n + phi - 1, n) + if p * q == n: + return p, q, d + + raise ValueError(f"Failed to find private exponent (max s = {max_s}, max r = {max_r}, max t = {max_t}).") diff --git a/rsa/hastad_attack.py b/rsa/hastad_attack.py new file mode 100644 index 0000000..5b18443 --- /dev/null +++ b/rsa/hastad_attack.py @@ -0,0 +1,27 @@ +from math import gcd + +from sage.all import Integer + + +def attack(e, moduli, ciphertexts): + """ + Recovers the plaintext from e ciphertexts, encrypted using different moduli and the same public exponent. + :param e: the public exponent + :param moduli: the moduli + :param ciphertexts: the ciphertexts + :return: the plaintext + """ + for i in range(len(moduli)): + for j in range(len(moduli)): + if i != j and gcd(moduli[i], moduli[j]) != 1: + raise ValueError(f"Modulus {i} and {j} share factors, Hastad's attack is impossible.") + + l = len(moduli) + p = 1 + for modulus in moduli: + p *= modulus + + n = list(map(lambda i: p // i, moduli)) + u = list(map(lambda i: pow(n[i], -1, moduli[i]), range(l))) + c = sum(map(lambda i: ciphertexts[i] * u[i] * n[i], range(l))) % p + return Integer(c).nth_root(e) diff --git a/rsa/low_exponent.py b/rsa/low_exponent.py new file mode 100644 index 0000000..f0d0644 --- /dev/null +++ b/rsa/low_exponent.py @@ -0,0 +1,11 @@ +from sage.all import Integer + + +def attack(e, c): + """ + Recovers the plaintext from a ciphertext, encrypted using a very small public exponent (e.g. e = 3). + :param e: the public exponent + :param c: the ciphertext + :return: the plaintext + """ + return Integer(c).nth_root(e) diff --git a/rsa/partial_key_exposure.py b/rsa/partial_key_exposure.py new file mode 100644 index 0000000..4fc55cc --- /dev/null +++ b/rsa/partial_key_exposure.py @@ -0,0 +1,44 @@ +import logging + +from sage.all import PolynomialRing +from sage.all import Zmod +from sage.all import solve_mod +from sage.all import var + +from small_roots.howgrave_graham import modular_univariate + + +def attack(n, e, bitsize, lsb_known, lsb): + """ + Recovers the prime factors of a modulus and the private exponent using Coppersmith's method if part of the private exponent is known. + More information: Boneh D., Durfee G., Frankel Y., "An Attack on RSA Given a Small Fraction of the Private Key Bits" + :param n: the modulus + :param e: the public exponent (should be "small": 3, 5, or 7 work best) + :param bitsize: the amount of bits of the prime factors + :param lsb_known: the amount of known least significant bits of the private exponent + :param lsb: the known least significant bits of the private exponent + :return: a tuple containing the prime factors of the modulus and the private exponent + """ + logging.debug("Generating solutions for k candidates...") + x = var("x") + solutions = [] + for k in range(1, e + 1): + solutions += solve_mod(k * x ** 2 + (e * lsb - k * (n + 1) - 1) * x + k * n == 0, 2 ** lsb_known) + + pr = PolynomialRing(Zmod(n), "x") + x = pr.gen() + bound = 2 ** (bitsize - lsb_known) - 1 + m = 1 + while True: + t = m + logging.debug(f"Trying m = {m}, t = {t}...") + for s in solutions: + p_lsb = int(s[0]) + f = x * 2 ** lsb_known + p_lsb + for root in modular_univariate(f, n, m, t, bound): + p = root * 2 ** lsb_known + p_lsb + if p != 0 and n % p == 0: + q = n // p + return p, q, pow(e, -1, (p - 1) * (q - 1)) + + m += 1 diff --git a/rsa/related_message.py b/rsa/related_message.py new file mode 100644 index 0000000..d475c2a --- /dev/null +++ b/rsa/related_message.py @@ -0,0 +1,28 @@ +from sage.all import PolynomialRing +from sage.all import Zmod + + +def _polynomial_gcd(a, b): + while b: + a, b = b, a % b + + return a.monic() + + +def attack(n, e, c1, c2, f1, f2): + """ + Recovers the shared secret from two plaintext messages encrypted with the same modulus. + :param n: the modulus + :param e: the public exponent + :param c1: the ciphertext of the first encryption + :param c2: the ciphertext of the second encryption + :param f1: the polynomial encoding the shared secret into the first plaintext + :param f2: the polynomial encoding the shared secret into the second plaintext + :return: the shared secret + """ + pr = PolynomialRing(Zmod(n), "x") + x = pr.gen() + g1 = f1(x) ** e - c1 + g2 = f2(x) ** e - c2 + g = -_polynomial_gcd(g1, g2) + return g[0] diff --git a/rsa/stereotyped_message.py b/rsa/stereotyped_message.py new file mode 100644 index 0000000..f5af44f --- /dev/null +++ b/rsa/stereotyped_message.py @@ -0,0 +1,33 @@ +import logging + +from sage.all import PolynomialRing +from sage.all import Zmod + +from small_roots.howgrave_graham import modular_univariate + + +def attack(n, e, c, bitsize, msb_known, msb, lsb_known, lsb): + """ + Recovers the plaintext from the ciphertext if some bits of the plaintext are known, using Coppersmith's method. + :param n: the modulus + :param e: the public exponent (should be "small": 3, 5, or 7 work best) + :param c: the encrypted message + :param bitsize: the amount of bits of the plaintext + :param msb_known: the amount of known most significant bits of the plaintext + :param msb: the known most significant bits of the plaintext + :param lsb_known: the amount of known least significant bits of the plaintext + :param lsb: the known least significant bits of the plaintext + :return: a tuple containing the prime factors + """ + pr = PolynomialRing(Zmod(n), "x") + x = pr.gen() + f = (msb * 2 ** (bitsize - msb_known) + x * 2 ** lsb_known + lsb) ** e - c + bound = 2 ** (bitsize - msb_known - lsb_known) - 1 + m = 1 + while True: + t = m + logging.debug(f"Trying m = {m}, t = {t}...") + for root in modular_univariate(f, n, m, t, bound): + return msb * 2 ** (bitsize - msb_known) + root * 2 ** lsb_known + lsb + + m += 1 diff --git a/rsa/wiener_attack.py b/rsa/wiener_attack.py new file mode 100644 index 0000000..7f90f67 --- /dev/null +++ b/rsa/wiener_attack.py @@ -0,0 +1,34 @@ +from math import isqrt + +from sage.all import Integer +from sage.all import continued_fraction + + +def _solve_quadratic(a, b, c): + d = b ** 2 - 4 * a * c + if d < 0: + return 0, 0 + else: + return (-b + isqrt(d)) // (2 * a), (-b - isqrt(d)) // (2 * a) + + +def wiener_attack(n, e): + """ + Recovers the prime factors of a modulus and the private exponent if the private exponent is too small. + :param n: the modulus + :param e: the public exponent + :return: a tuple containing the prime factors of the modulus and the private exponent + """ + convergents = continued_fraction(Integer(e) / Integer(n)).convergents() + for c in convergents: + k = c.numerator() + d = c.denominator() + if k == 0 or (e * d - 1) % k != 0: + continue + + phi = (e * d - 1) // k + p, q = _solve_quadratic(1, -n + phi - 1, n) + if p * q == n: + return p, q, d + + raise ValueError("Failed to find private exponent.") diff --git a/shamir_secret_sharing/deterministic_coefficients.py b/shamir_secret_sharing/deterministic_coefficients.py new file mode 100644 index 0000000..59cea70 --- /dev/null +++ b/shamir_secret_sharing/deterministic_coefficients.py @@ -0,0 +1,18 @@ +def attack(p, k, a1, f, x, y): + """ + Recovers the shared secret if the coefficients are generated deterministically, and a single share is given. + :param p: the prime used for Shamir's secret sharing + :param k: the amount of shares needed to unlock the secret + :param a1: the first coefficient of the polynomial + :param f: a function which takes a coefficient and returns the next coefficient in the polynomial + :param x: the x coordinate of the given share + :param y: the y coordinate of the given share + :return: the shared secret + """ + s = y + a = a1 + for i in range(1, k): + s -= a * x ** i + a = f(a) + + return s % p diff --git a/shamir_secret_sharing/share_forgery.py b/shamir_secret_sharing/share_forgery.py new file mode 100644 index 0000000..04b3be1 --- /dev/null +++ b/shamir_secret_sharing/share_forgery.py @@ -0,0 +1,16 @@ +def attack(p, s, s_, x, y, xs): + """ + 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. + :param p: the prime used for Shamir's secret sharing + :param s: the original shared secret + :param s_: the target shared secret, s' + :param x: the x coordinate of the given share + :param y: the y coordinate of the given share + :param xs: the x coordinates of the other participants (excluding the x coordinate of the given share) + :return: + """ + const = 1 + for i in xs: + const *= i * pow(i - x, -1, p) + + return ((s_ - s) * pow(const, -1, p) + y) % p diff --git a/small_roots/boneh_durfee.py b/small_roots/boneh_durfee.py new file mode 100644 index 0000000..8ea4849 --- /dev/null +++ b/small_roots/boneh_durfee.py @@ -0,0 +1,76 @@ +import logging + +from sage.all import Matrix +from sage.all import ZZ + + +def modular_bivariate(f, modulus, m, t, xbound, ybound): + """ + Computes small modular roots of a bivariate polynomial. + More information: Boneh D., Durfee G., "Cryptanalysis of RSA with Private Key d Less than N^0.292" + :param f: the polynomial + :param modulus: the modulus + :param m: the amount of normal shifts to use + :param t: the amount of additional shifts to use + :param xbound: an approximate bound on the x roots + :param ybound: an approximate bound on the y roots + :return: a generator generating small roots (tuples of x and y roots) of the polynomial + """ + f = f.change_ring(ZZ) + x, y = f.parent().gens() + + shifts = [] + monomials = [] + logging.debug("Generating x shifts...") + for i in range(m + 1): + for j in range(i + 1): + shift = x ** (i - j) * f ** j * modulus ** (m - j) + for monomial in shift.monomials(): + if monomial not in monomials: + helpful = shift.monomial_coefficient(monomial) * monomial(xbound, ybound) < modulus ** m + monomials.append(monomial) + + if shift not in shifts: + shifts.append(shift) + + logging.debug("Generating y shifts...") + for i in range(1, t + 1): + for j in range(m + 1): + shift = y ** i * f ** j * modulus ** (m - j) + for monomial in shift.monomials(): + if monomial not in monomials: + helpful = shift.monomial_coefficient(monomial) * monomial(xbound, ybound) < modulus ** m + monomials.append(monomial) + + if shift not in shifts and helpful: + shifts.append(shift) + + logging.debug(f"Filling the lattice ({len(shifts)} x {len(monomials)})...") + latticce = Matrix(len(shifts), len(monomials)) + for row, shift in enumerate(shifts): + for col, monomial in enumerate(monomials): + latticce[row, col] = shift.monomial_coefficient(monomial) * monomial(xbound, ybound) + + logging.debug("Executing the LLL algorithm...") + basis = latticce.LLL() + + logging.debug("Reconstructing polynomials...") + new_polynomials = [] + for row in range(basis.nrows()): + # Reconstruct the polynomial from reduced basis + new_polynomial = 0 + for col, monomial in enumerate(monomials): + new_polynomial += basis[row, col] * monomial // monomial(xbound, ybound) + + new_polynomials.append(new_polynomial) + + logging.debug("Generating resultants...") + for p1 in new_polynomials: + for p2 in new_polynomials: + resultant = p1.resultant(p2, y) + if not resultant.is_constant(): + for xroot in resultant.univariate_polynomial().roots(): + xroot = int(xroot[0]) + for yroot in p1.subs(x=xroot).univariate_polynomial().roots(): + yroot = int(yroot[0]) + yield xroot, yroot diff --git a/small_roots/coron.py b/small_roots/coron.py new file mode 100644 index 0000000..e41d9e2 --- /dev/null +++ b/small_roots/coron.py @@ -0,0 +1,92 @@ +import logging + +from sage.all import Matrix + + +def integer_bivariate(f, k, xbound, ybound): + """ + Computes small integer roots of a bivariate polynomial. + More information: Coron J., "Finding Small Roots of Bivariate Integer Polynomial Equations: a Direct Approach" + :param f: the polynomial + :param k: the amount of shifts to use + :param xbound: an approximate bound on the x roots + :param ybound: an approximate bound on the y roots + :return: a generator generating small roots (tuples of x and y roots) of the polynomial + """ + x, y = f.parent().gens() + d = max(f.degrees()) + + W = 0 + i0 = 0 + j0 = 0 + for i in range(d + 1): + for j in range(d + 1): + w = abs(int(f.coefficient([i, j]))) * xbound ** i * ybound ** j + if w > W: + W = w + i0 = i + j0 = j + + logging.debug("Calculating n...") + S = Matrix(k ** 2) + for a in range(k): + for b in range(k): + s = x ** a * y ** b * f + for i in range(k): + for j in range(k): + S[a * k + b, i * k + j] = int(s.coefficient([i0 + i, j0 + j])) + + n = abs(S.det()) + logging.debug(f"Found n = {n}") + + # Monomials are collected in "left" and "right" lists, which determine where the columns are in relation to eachother + # This partition ensures the Hermite form will set desired monomial coefficients to zero + logging.debug("Generating monomials...") + left_monomials = [] + right_monomials = [] + for i in range(k + d): + for j in range(k + d): + if 0 <= i - i0 < k and 0 <= j - j0 < k: + left_monomials.append(x ** i * y ** j) + else: + right_monomials.append(x ** i * y ** j) + + assert len(left_monomials) == k ** 2 + monomials = left_monomials + right_monomials + + lattice = Matrix(k ** 2 + (k + d) ** 2, (k + d) ** 2) + row = 0 + logging.debug("Generating normal shifts...") + for a in range(k): + for b in range(k): + shift = x ** a * y ** b * f + for col, monomial in enumerate(monomials): + lattice[row, col] = shift.monomial_coefficient(monomial) * monomial(xbound, ybound) + + row += 1 + + logging.debug("Generating additional shifts...") + for col, monomial in enumerate(monomials): + lattice[row, col] = n * monomial(xbound, ybound) + row += 1 + + logging.debug("Generating Hermite form...") + lattice = lattice.hermite_form() + + logging.debug("Executing the LLL algorithm on the sublattice...") + basis = lattice.submatrix(k ** 2, k ** 2, (k + d) ** 2 - k ** 2).LLL() + + logging.debug("Reconstructing polynomials...") + for row in range(basis.nrows()): + new_polynomial = 0 + # Only use right monomials now (corresponding the the sublattice) + for col, monomial in enumerate(right_monomials): + new_polynomial += basis[row, col] * monomial // monomial(xbound, ybound) + + resultant = new_polynomial.resultant(f, y) + if not resultant.is_constant(): + for xroot in resultant.univariate_polynomial().roots(): + xroot = int(xroot[0]) + for yroot in f.subs(x=xroot).univariate_polynomial().roots(): + yroot = int(yroot[0]) + yield xroot, yroot diff --git a/small_roots/howgrave_graham.py b/small_roots/howgrave_graham.py new file mode 100644 index 0000000..be4bc4d --- /dev/null +++ b/small_roots/howgrave_graham.py @@ -0,0 +1,51 @@ +import logging + +from sage.all import Matrix +from sage.all import ZZ + + +def modular_univariate(f, modulus, m, t, bound): + """ + Computes small modular roots of a univariate polynomial. + More information: May A., "New RSA Vulnerabilities Using Lattice Reduction Methods" + :param f: the polynomial + :param modulus: the modulus + :param m: the amount of normal shifts to use + :param t: the amount of additional shifts to use + :param bound: an approximate bound on the roots + :return: a generator generating small roots of the polynomial + """ + f = f.monic().change_ring(ZZ) + x = f.parent().gen() + d = f.degree() + + lattice = Matrix(d * m + t) + row = 0 + logging.debug("Generating normal shifts...") + for i in range(m): + for j in range(d): + shift = (x * bound) ** j * modulus ** (m - i) * f(x * bound) ** i + for col in range(row + 1): + lattice[row, col] = shift[col] + + row += 1 + + logging.debug("Generating additional shifts...") + for i in range(t): + shift = (x * bound) ** i * f(x * bound) ** m + for col in range(row + 1): + lattice[row, col] = shift[col] + + row += 1 + + logging.debug("Executing the LLL algorithm...") + basis = lattice.LLL() + + logging.debug("Reconstructing polynomials...") + for row in range(basis.nrows()): + new_polynomial = 0 + for col in range(basis.ncols()): + new_polynomial += (basis[row, col] // bound ** col) * x ** col + + for root in new_polynomial.roots(): + yield int(root[0])