From f096857ce0e9e4261bf555b73154cd39e30e66e9 Mon Sep 17 00:00:00 2001 From: grodik Date: Sat, 9 Oct 2021 18:14:44 -0400 Subject: [PATCH 1/2] Prelim minimization work --- adafruit_rsa/common.py | 263 ++++++++++++++++++++++++++++++++++++++ adafruit_rsa/key.py | 5 +- adafruit_rsa/pkcs1.py | 22 ++-- adafruit_rsa/prime.py | 1 - adafruit_rsa/randnum.py | 89 ------------- adafruit_rsa/transform.py | 200 ----------------------------- 6 files changed, 276 insertions(+), 304 deletions(-) delete mode 100755 adafruit_rsa/randnum.py delete mode 100755 adafruit_rsa/transform.py diff --git a/adafruit_rsa/common.py b/adafruit_rsa/common.py index 295e92c..2adc980 100755 --- a/adafruit_rsa/common.py +++ b/adafruit_rsa/common.py @@ -7,6 +7,16 @@ # pylint: disable=invalid-name +import os + +from adafruit_rsa._compat import byte + +from struct import pack +import adafruit_binascii as binascii + +from adafruit_rsa._compat import byte, is_integer +from adafruit_rsa import machine_size + __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RSA.git" @@ -186,3 +196,256 @@ def crt(a_values, modulo_values): x = (x + a_i * M_i * inv) % m return x + + +def read_random_bits(nbits): + """Reads 'nbits' random bits. + + If nbits isn't a whole number of bytes, an extra byte will be appended with + only the lower bits set. + """ + + nbytes, rbits = divmod(nbits, 8) + + # Get the random bytes + randomdata = os.urandom(nbytes) + + # Add the remaining random bits + if rbits > 0: + randomvalue = ord(os.urandom(1)) + randomvalue >>= 8 - rbits + randomdata = byte(randomvalue) + randomdata + + return randomdata + + +def read_random_int(nbits): + """Reads a random integer of approximately nbits bits.""" + + randomdata = read_random_bits(nbits) + value = bytes2int(randomdata) + + # Ensure that the number is large enough to just fill out the required + # number of bits. + value |= 1 << (nbits - 1) + + return value + + +def read_random_odd_int(nbits): + """Reads a random odd integer of approximately nbits bits. + + >>> read_random_odd_int(512) & 1 + 1 + """ + + value = read_random_int(nbits) + + # Make sure it's odd + return value | 1 + + +def randint(maxvalue): + """Returns a random integer x with 1 <= x <= maxvalue + + May take a very long time in specific situations. If maxvalue needs N bits + to store, the closer maxvalue is to (2 ** N) - 1, the faster this function + is. + """ + + bit_size = bit_size(maxvalue) + + tries = 0 + while True: + value = read_random_int(bit_size) + if value <= maxvalue: + break + + if tries % 10 == 0 and tries: + # After a lot of tries to get the right number of bits but still + # smaller than maxvalue, decrease the number of bits by 1. That'll + # dramatically increase the chances to get a large enough number. + bit_size -= 1 + tries += 1 + + return value + + +def bytes2int(raw_bytes): + """Converts a list of bytes or an 8-bit string to an integer. + + When using unicode strings, encode it to some encoding like UTF8 first. + + >>> (((128 * 256) + 64) * 256) + 15 + 8405007 + >>> bytes2int(b'\x80@\x0f') + 8405007 + + """ + + return int(binascii.hexlify(raw_bytes), 16) + + +def _int2bytes(number, block_size=None): + """Converts a number to a string of bytes. + + Usage:: + + >>> _int2bytes(123456789) + b'\x07[\xcd\x15' + >>> bytes2int(_int2bytes(123456789)) + 123456789 + + >>> _int2bytes(123456789, 6) + b'\x00\x00\x07[\xcd\x15' + >>> bytes2int(_int2bytes(123456789, 128)) + 123456789 + + >>> _int2bytes(123456789, 3) + Traceback (most recent call last): + ... + OverflowError: Needed 4 bytes for number, but block size is 3 + + @param number: the number to convert + @param block_size: the number of bytes to output. If the number encoded to + bytes is less than this, the block will be zero-padded. When not given, + the returned block is not padded. + + @throws OverflowError when block_size is given and the number takes up more + bytes than fit into the block. + """ + + # Type checking + if not is_integer(number): + raise TypeError( + "You must pass an integer for 'number', not %s" % number.__class__ + ) + + if number < 0: + raise ValueError("Negative numbers cannot be used: %i" % number) + + # Do some bounds checking + if number == 0: + needed_bytes = 1 + raw_bytes = [b"\x00"] + else: + needed_bytes = common.byte_size(number) + raw_bytes = [] + + # You cannot compare None > 0 in Python 3x. It will fail with a TypeError. + if block_size and block_size > 0: + if needed_bytes > block_size: + raise OverflowError( + "Needed %i bytes for number, but block size " + "is %i" % (needed_bytes, block_size) + ) + + # Convert the number to bytes. + while number > 0: + raw_bytes.insert(0, byte(number & 0xFF)) + number >>= 8 + + # Pad with zeroes to fill the block + if block_size and block_size > 0: + padding = (block_size - needed_bytes) * b"\x00" + else: + padding = b"" + + return padding + b"".join(raw_bytes) + + +def bytes_leading(raw_bytes, needle=b"\x00"): + """ + Finds the number of prefixed byte occurrences in the haystack. + + Useful when you want to deal with padding. + + :param raw_bytes: + Raw bytes. + :param needle: + The byte to count. Default \x00. + :returns: + The number of leading needle bytes. + """ + + leading = 0 + # Indexing keeps compatibility between Python 2.x and Python 3.x + _byte = needle[0] + for x in raw_bytes: + if x == _byte: + leading += 1 + else: + break + return leading + + +def int2bytes(number, fill_size=None, chunk_size=None, overflow=False): + """ + Convert an unsigned integer to bytes (base-256 representation):: + Does not preserve leading zeros if you don't specify a chunk size or + fill size. + .. NOTE: + You must not specify both fill_size and chunk_size. Only one + of them is allowed. + :param number: + Integer value + :param fill_size: + If the optional fill size is given the length of the resulting + byte string is expected to be the fill size and will be padded + with prefix zero bytes to satisfy that length. + :param chunk_size: + If optional chunk size is given and greater than zero, pad the front of + the byte string with binary zeros so that the length is a multiple of + ``chunk_size``. + :param overflow: + ``False`` (default). If this is ``True``, no ``OverflowError`` + will be raised when the fill_size is shorter than the length + of the generated byte sequence. Instead the byte sequence will + be returned as is. + :returns: + Raw bytes (base-256 representation). + :raises: + ``OverflowError`` when fill_size is given and the number takes up more + bytes than fit into the block. This requires the ``overflow`` + argument to this function to be set to ``False`` otherwise, no + error will be raised. + """ + + if number < 0: + raise ValueError("Number must be an unsigned integer: %d" % number) + + if fill_size and chunk_size: + raise ValueError("You can either fill or pad chunks, but not both") + + # Ensure these are integers. + assert isinstance(number, int), "Number must be an unsigned integer, not a float." + + raw_bytes = b"" + + # Pack the integer one machine word at a time into bytes. + num = number + word_bits, _, max_uint, pack_type = machine_size.get_word_alignment(num) + pack_format = ">%s" % pack_type + while num > 0: + raw_bytes = pack(pack_format, num & max_uint) + raw_bytes + num >>= word_bits + # Obtain the index of the first non-zero byte. + zero_leading = bytes_leading(raw_bytes) + if number == 0: + raw_bytes = b"\x00" + # De-padding. + raw_bytes = raw_bytes[zero_leading:] + + length = len(raw_bytes) + if fill_size and fill_size > 0: + if not overflow and length > fill_size: + raise OverflowError( + "Need %d bytes for number, but fill size is %d" % (length, fill_size) + ) + raw_bytes = (b"\x00" * (fill_size - len(raw_bytes))) + raw_bytes + elif chunk_size and chunk_size > 0: + remainder = length % chunk_size + if remainder: + padding_size = chunk_size - remainder + raw_bytes = "% {}s".format(length + padding_size).encode() % raw_bytes + return raw_bytes diff --git a/adafruit_rsa/key.py b/adafruit_rsa/key.py index a1ec7c3..699c9cb 100755 --- a/adafruit_rsa/key.py +++ b/adafruit_rsa/key.py @@ -27,7 +27,6 @@ import adafruit_rsa.prime import adafruit_rsa.pem import adafruit_rsa.common -import adafruit_rsa.randnum import adafruit_rsa.core __version__ = "0.0.0-auto.0" @@ -434,7 +433,7 @@ def blinded_decrypt(self, encrypted): :rtype: int """ - blind_r = adafruit_rsa.randnum.randint(self.n - 1) + blind_r = adafruit_rsa.common.randint(self.n - 1) blinded = self.blind(encrypted, blind_r) # blind before decrypting decrypted = adafruit_rsa.core.decrypt_int(blinded, self.d, self.n) @@ -450,7 +449,7 @@ def blinded_encrypt(self, message): :rtype: int """ - blind_r = adafruit_rsa.randnum.randint(self.n - 1) + blind_r = adafruit_rsa.common.randint(self.n - 1) blinded = self.blind(message, blind_r) # blind before encrypting encrypted = adafruit_rsa.core.encrypt_int(blinded, self.d, self.n) return self.unblind(encrypted, blind_r) diff --git a/adafruit_rsa/pkcs1.py b/adafruit_rsa/pkcs1.py index 8b7339e..4ecd55d 100755 --- a/adafruit_rsa/pkcs1.py +++ b/adafruit_rsa/pkcs1.py @@ -18,7 +18,7 @@ """ import os import adafruit_hashlib as hashlib -from adafruit_rsa import common, transform, core +from adafruit_rsa import common, core __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RSA.git" @@ -157,9 +157,9 @@ def encrypt(message, pub_key): keylength = common.byte_size(pub_key.n) padded = _pad_for_encryption(message, keylength) - payload = transform.bytes2int(padded) + payload = common.bytes2int(padded) encrypted = core.encrypt_int(payload, pub_key.e, pub_key.n) - block = transform.int2bytes(encrypted, keylength) + block = common.int2bytes(encrypted, keylength) return block @@ -216,9 +216,9 @@ def decrypt(crypto, priv_key): """ blocksize = common.byte_size(priv_key.n) - encrypted = transform.bytes2int(crypto) + encrypted = common.bytes2int(crypto) decrypted = priv_key.blinded_decrypt(encrypted) - cleartext = transform.int2bytes(decrypted, blocksize) + cleartext = common.int2bytes(decrypted, blocksize) # Find the 00 separator between the padding and the message try: @@ -256,9 +256,9 @@ def sign_hash(hash_value, priv_key, hash_method): keylength = common.byte_size(priv_key.n) padded = _pad_for_signing(cleartext, keylength) - payload = transform.bytes2int(padded) + payload = common.bytes2int(padded) encrypted = priv_key.blinded_encrypt(payload) - block = transform.int2bytes(encrypted, keylength) + block = common.int2bytes(encrypted, keylength) return block @@ -301,9 +301,9 @@ def verify(message, signature, pub_key): """ keylength = common.byte_size(pub_key.n) - encrypted = transform.bytes2int(signature) + encrypted = common.bytes2int(signature) decrypted = core.decrypt_int(encrypted, pub_key.e, pub_key.n) - clearsig = transform.int2bytes(decrypted, keylength) + clearsig = common.int2bytes(decrypted, keylength) # Get the hash method method_name = _find_method_hash(clearsig) @@ -332,9 +332,9 @@ def find_signature_hash(signature, pub_key): """ keylength = common.byte_size(pub_key.n) - encrypted = transform.bytes2int(signature) + encrypted = common.bytes2int(signature) decrypted = core.decrypt_int(encrypted, pub_key.e, pub_key.n) - clearsig = transform.int2bytes(decrypted, keylength) + clearsig = common.int2bytes(decrypted, keylength) return _find_method_hash(clearsig) diff --git a/adafruit_rsa/prime.py b/adafruit_rsa/prime.py index 003719b..d776ca8 100755 --- a/adafruit_rsa/prime.py +++ b/adafruit_rsa/prime.py @@ -10,7 +10,6 @@ """ # pylint: disable=invalid-name import adafruit_rsa.common -import adafruit_rsa.randnum __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RSA.git" diff --git a/adafruit_rsa/randnum.py b/adafruit_rsa/randnum.py deleted file mode 100755 index 6fddb4b..0000000 --- a/adafruit_rsa/randnum.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-FileCopyrightText: 2011 Sybren A. Stüvel -# -# SPDX-License-Identifier: Apache-2.0 - -"""Functions for generating random numbers.""" - -# Source inspired by code by Yesudeep Mangalapilly - -import os - -from adafruit_rsa import common, transform -from adafruit_rsa._compat import byte - -__version__ = "0.0.0-auto.0" -__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RSA.git" - - -def read_random_bits(nbits): - """Reads 'nbits' random bits. - - If nbits isn't a whole number of bytes, an extra byte will be appended with - only the lower bits set. - """ - - nbytes, rbits = divmod(nbits, 8) - - # Get the random bytes - randomdata = os.urandom(nbytes) - - # Add the remaining random bits - if rbits > 0: - randomvalue = ord(os.urandom(1)) - randomvalue >>= 8 - rbits - randomdata = byte(randomvalue) + randomdata - - return randomdata - - -def read_random_int(nbits): - """Reads a random integer of approximately nbits bits.""" - - randomdata = read_random_bits(nbits) - value = transform.bytes2int(randomdata) - - # Ensure that the number is large enough to just fill out the required - # number of bits. - value |= 1 << (nbits - 1) - - return value - - -def read_random_odd_int(nbits): - """Reads a random odd integer of approximately nbits bits. - - >>> read_random_odd_int(512) & 1 - 1 - """ - - value = read_random_int(nbits) - - # Make sure it's odd - return value | 1 - - -def randint(maxvalue): - """Returns a random integer x with 1 <= x <= maxvalue - - May take a very long time in specific situations. If maxvalue needs N bits - to store, the closer maxvalue is to (2 ** N) - 1, the faster this function - is. - """ - - bit_size = common.bit_size(maxvalue) - - tries = 0 - while True: - value = read_random_int(bit_size) - if value <= maxvalue: - break - - if tries % 10 == 0 and tries: - # After a lot of tries to get the right number of bits but still - # smaller than maxvalue, decrease the number of bits by 1. That'll - # dramatically increase the chances to get a large enough number. - bit_size -= 1 - tries += 1 - - return value diff --git a/adafruit_rsa/transform.py b/adafruit_rsa/transform.py deleted file mode 100755 index a94828c..0000000 --- a/adafruit_rsa/transform.py +++ /dev/null @@ -1,200 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-FileCopyrightText: 2011 Sybren A. Stüvel -# -# SPDX-License-Identifier: Apache-2.0 - -"""Data transformation functions. - -From bytes to a number, number to bytes, etc. -""" - -# from __future__ import absolute_import - -from struct import pack -import adafruit_binascii as binascii - -from adafruit_rsa._compat import byte, is_integer -from adafruit_rsa import common, machine_size - -__version__ = "0.0.0-auto.0" -__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RSA.git" - - -def bytes2int(raw_bytes): - """Converts a list of bytes or an 8-bit string to an integer. - - When using unicode strings, encode it to some encoding like UTF8 first. - - >>> (((128 * 256) + 64) * 256) + 15 - 8405007 - >>> bytes2int(b'\x80@\x0f') - 8405007 - - """ - - return int(binascii.hexlify(raw_bytes), 16) - - -def _int2bytes(number, block_size=None): - """Converts a number to a string of bytes. - - Usage:: - - >>> _int2bytes(123456789) - b'\x07[\xcd\x15' - >>> bytes2int(_int2bytes(123456789)) - 123456789 - - >>> _int2bytes(123456789, 6) - b'\x00\x00\x07[\xcd\x15' - >>> bytes2int(_int2bytes(123456789, 128)) - 123456789 - - >>> _int2bytes(123456789, 3) - Traceback (most recent call last): - ... - OverflowError: Needed 4 bytes for number, but block size is 3 - - @param number: the number to convert - @param block_size: the number of bytes to output. If the number encoded to - bytes is less than this, the block will be zero-padded. When not given, - the returned block is not padded. - - @throws OverflowError when block_size is given and the number takes up more - bytes than fit into the block. - """ - - # Type checking - if not is_integer(number): - raise TypeError( - "You must pass an integer for 'number', not %s" % number.__class__ - ) - - if number < 0: - raise ValueError("Negative numbers cannot be used: %i" % number) - - # Do some bounds checking - if number == 0: - needed_bytes = 1 - raw_bytes = [b"\x00"] - else: - needed_bytes = common.byte_size(number) - raw_bytes = [] - - # You cannot compare None > 0 in Python 3x. It will fail with a TypeError. - if block_size and block_size > 0: - if needed_bytes > block_size: - raise OverflowError( - "Needed %i bytes for number, but block size " - "is %i" % (needed_bytes, block_size) - ) - - # Convert the number to bytes. - while number > 0: - raw_bytes.insert(0, byte(number & 0xFF)) - number >>= 8 - - # Pad with zeroes to fill the block - if block_size and block_size > 0: - padding = (block_size - needed_bytes) * b"\x00" - else: - padding = b"" - - return padding + b"".join(raw_bytes) - - -def bytes_leading(raw_bytes, needle=b"\x00"): - """ - Finds the number of prefixed byte occurrences in the haystack. - - Useful when you want to deal with padding. - - :param raw_bytes: - Raw bytes. - :param needle: - The byte to count. Default \x00. - :returns: - The number of leading needle bytes. - """ - - leading = 0 - # Indexing keeps compatibility between Python 2.x and Python 3.x - _byte = needle[0] - for x in raw_bytes: - if x == _byte: - leading += 1 - else: - break - return leading - - -def int2bytes(number, fill_size=None, chunk_size=None, overflow=False): - """ - Convert an unsigned integer to bytes (base-256 representation):: - Does not preserve leading zeros if you don't specify a chunk size or - fill size. - .. NOTE: - You must not specify both fill_size and chunk_size. Only one - of them is allowed. - :param number: - Integer value - :param fill_size: - If the optional fill size is given the length of the resulting - byte string is expected to be the fill size and will be padded - with prefix zero bytes to satisfy that length. - :param chunk_size: - If optional chunk size is given and greater than zero, pad the front of - the byte string with binary zeros so that the length is a multiple of - ``chunk_size``. - :param overflow: - ``False`` (default). If this is ``True``, no ``OverflowError`` - will be raised when the fill_size is shorter than the length - of the generated byte sequence. Instead the byte sequence will - be returned as is. - :returns: - Raw bytes (base-256 representation). - :raises: - ``OverflowError`` when fill_size is given and the number takes up more - bytes than fit into the block. This requires the ``overflow`` - argument to this function to be set to ``False`` otherwise, no - error will be raised. - """ - - if number < 0: - raise ValueError("Number must be an unsigned integer: %d" % number) - - if fill_size and chunk_size: - raise ValueError("You can either fill or pad chunks, but not both") - - # Ensure these are integers. - assert isinstance(number, int), "Number must be an unsigned integer, not a float." - - raw_bytes = b"" - - # Pack the integer one machine word at a time into bytes. - num = number - word_bits, _, max_uint, pack_type = machine_size.get_word_alignment(num) - pack_format = ">%s" % pack_type - while num > 0: - raw_bytes = pack(pack_format, num & max_uint) + raw_bytes - num >>= word_bits - # Obtain the index of the first non-zero byte. - zero_leading = bytes_leading(raw_bytes) - if number == 0: - raw_bytes = b"\x00" - # De-padding. - raw_bytes = raw_bytes[zero_leading:] - - length = len(raw_bytes) - if fill_size and fill_size > 0: - if not overflow and length > fill_size: - raise OverflowError( - "Need %d bytes for number, but fill size is %d" % (length, fill_size) - ) - raw_bytes = (b"\x00" * (fill_size - len(raw_bytes))) + raw_bytes - elif chunk_size and chunk_size > 0: - remainder = length % chunk_size - if remainder: - padding_size = chunk_size - remainder - raw_bytes = "% {}s".format(length + padding_size).encode() % raw_bytes - return raw_bytes From 3f1e0045d7556f52b8d6f1a58600d767e769f81b Mon Sep 17 00:00:00 2001 From: grodik Date: Mon, 11 Oct 2021 17:44:30 -0400 Subject: [PATCH 2/2] Fix a few more issues --- adafruit_rsa/common.py | 28 +++++++++++++--------------- adafruit_rsa/key.py | 5 +++-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/adafruit_rsa/common.py b/adafruit_rsa/common.py index 2adc980..ce46a10 100755 --- a/adafruit_rsa/common.py +++ b/adafruit_rsa/common.py @@ -9,8 +9,6 @@ import os -from adafruit_rsa._compat import byte - from struct import pack import adafruit_binascii as binascii @@ -21,16 +19,6 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RSA.git" -def bit_length(int_type): - """Return the number of bits necessary to represent an integer in binary, - excluding the sign and leading zeros""" - length = 0 - while int_type: - int_type >>= 1 - length += 1 - return length - - class NotRelativePrimeError(ValueError): """Raises if provided a and b not relatively prime.""" @@ -43,6 +31,16 @@ def __init__(self, a, b, d, msg=None): self.d = d +def bit_length(int_type): + """Return the number of bits necessary to represent an integer in binary, + excluding the sign and leading zeros""" + length = 0 + while int_type: + int_type >>= 1 + length += 1 + return length + + def bit_size(num): """ Number of bits needed to represent a integer excluding any prefix @@ -253,11 +251,11 @@ def randint(maxvalue): is. """ - bit_size = bit_size(maxvalue) + _bit_size = bit_size(maxvalue) tries = 0 while True: - value = read_random_int(bit_size) + value = read_random_int(_bit_size) if value <= maxvalue: break @@ -265,7 +263,7 @@ def randint(maxvalue): # After a lot of tries to get the right number of bits but still # smaller than maxvalue, decrease the number of bits by 1. That'll # dramatically increase the chances to get a large enough number. - bit_size -= 1 + _bit_size -= 1 tries += 1 return value diff --git a/adafruit_rsa/key.py b/adafruit_rsa/key.py index 699c9cb..f50f9f5 100755 --- a/adafruit_rsa/key.py +++ b/adafruit_rsa/key.py @@ -29,6 +29,7 @@ import adafruit_rsa.common import adafruit_rsa.core + __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RSA.git" @@ -249,7 +250,7 @@ def _load_pkcs1_der(cls, keyfile): """ # pylint: disable=import-outside-toplevel - from adafruit_rsa.tools.pyasn1.codec.der import decoder + from pyasn1.codec.der import decoder from adafruit_rsa.asn1 import AsnPubKey (priv, _) = decoder.decode(keyfile, asn1Spec=AsnPubKey()) @@ -476,7 +477,7 @@ def _load_pkcs1_der(cls, keyfile): """ - from adafruit_rsa.tools.pyasn1.codec.der import ( # pylint: disable=import-outside-toplevel + from pyasn1.codec.der import ( # pylint: disable=import-outside-toplevel decoder, )