diff --git a/.codeclimate.yml b/.codeclimate.yml index 92ec005..077382c 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -15,4 +15,13 @@ ratings: - "**.py" exclude_paths: - tests/**/* -- rsa/_version*.py +checks: + argument-count: + config: + threshold: 5 + file-lines: + config: + threshold: 1000 + method-complexity: + config: + threshold: 17 diff --git a/.gitignore b/.gitignore index a90d26c..e31443a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,10 +8,13 @@ /distribute*.tar.gz /distribute*.egg -/.tox/ -/.coverage -/.coverage.* -/.cache/ +.tox/ +.coverage +.coverage.* +.cache/ +.mypy_cache/ +.pytest_cache/ +__pycache__/ /build/ /doc/_build/ diff --git a/.travis.yml b/.travis.yml index 5304305..42d4d16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,30 @@ language: python - -# Python 3.5 specified to make tox environment 'py35' work. -# See: https://github.com/travis-ci/travis-ci/issues/4794 -python: - - 3.5 +dist: bionic # required for Python >= 3.7 +cache: pip # Environment changes have to be manually synced with 'tox.ini'. # See: https://github.com/travis-ci/travis-ci/issues/3024 -env: - - TOXENV=py26 - - TOXENV=py27 - - TOXENV=py33 - - TOXENV=py34 - - TOXENV=py35 - - TOXENV=pypy + +python: + - "3.6" + - "3.7" + - "3.8" + +matrix: + include: + - python: 3.5 + dist: xenial # Bionic has no Python 3.5 + + # Disabled, see https://github.com/sybrenstuvel/python-rsa/issues/131 + #- python: pypy3.5 + # dist: xenial # Bionic has no Python 3.5 install: - - pip install -r requirements.txt - - pip install coveralls + - pip install poetry + - poetry install -v script: - - tox + - poetry run py.test tests/ after_success: - # Coveralls submission only for py35 environment, because of being the only - # one that executes doctest-modules testing, according to tox.ini. - - if [ ${TOXENV} = "py35" ]; then coveralls; fi + - poetry run coveralls diff --git a/CHANGELOG.txt b/CHANGELOG.md similarity index 52% rename from CHANGELOG.txt rename to CHANGELOG.md index 49c8ab2..0b263e2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.md @@ -1,23 +1,62 @@ -Python-RSA changelog -======================================== - -Version 3.4.2 - released 2016-03-29 ----------------------------------------- +# Python-RSA changelog + + +## Version 4.1 - released 2020-06-10 + +- Added support for Python 3.8. +- Dropped support for Python 2 and 3.4. +- Added type annotations to the source code. This will make Python-RSA easier to use in + your IDE, and allows better type checking. +- Added static type checking via [MyPy](http://mypy-lang.org/). +- Fix [#129](https://github.com/sybrenstuvel/python-rsa/issues/129) Installing from source + gives UnicodeDecodeError. +- Switched to using [Poetry](https://poetry.eustace.io/) for package + management. +- Added support for SHA3 hashing: SHA3-256, SHA3-384, SHA3-512. This + is natively supported by Python 3.6+ and supported via a third-party + library on Python 3.5. +- Choose blinding factor relatively prime to N. Thanks Christian Heimes for pointing this out. +- Reject cyphertexts (when decrypting) and signatures (when verifying) that have + been modified by prepending zero bytes. This resolves CVE-2020-13757. Thanks + Adelapie for pointing this out. + + +## Version 4.0 - released 2018-09-16 + +- Removed deprecated modules: + - rsa.varblock + - rsa.bigfile + - rsa._version133 + - rsa._version200 +- Removed CLI commands that use the VARBLOCK/bigfile format. +- Ensured that PublicKey.save_pkcs1() and PrivateKey.save_pkcs1() always return bytes. +- Dropped support for Python 2.6 and 3.3. +- Dropped support for Psyco. +- Miller-Rabin iterations determined by bitsize of key. + [#58](https://github.com/sybrenstuvel/python-rsa/pull/58) +- Added function `rsa.find_signature_hash()` to return the name of the hashing + algorithm used to sign a message. `rsa.verify()` now also returns that name, + instead of always returning `True`. + [#78](https://github.com/sybrenstuvel/python-rsa/issues/13) +- Add support for SHA-224 for PKCS1 signatures. + [#104](https://github.com/sybrenstuvel/python-rsa/pull/104) +- Transitioned from `requirements.txt` to Pipenv for package management. + + +## Version 3.4.2 - released 2016-03-29 - Fixed dates in CHANGELOG.txt -Version 3.4.1 - released 2016-03-26 ----------------------------------------- +## Version 3.4.1 - released 2016-03-26 - Included tests/private.pem in MANIFEST.in - Included README.md and CHANGELOG.txt in MANIFEST.in -Version 3.4 - released 2016-03-17 ----------------------------------------- +## Version 3.4 - released 2016-03-17 -- Moved development to Github: https://github.com/sybrenstuvel/python-rsa +- Moved development to GitHub: https://github.com/sybrenstuvel/python-rsa - Solved side-channel vulnerability by implementing blinding, fixes #19 - Deprecated the VARBLOCK format and rsa.bigfile module due to security issues, see https://github.com/sybrenstuvel/python-rsa/issues/13 @@ -33,11 +72,10 @@ Version 3.4 - released 2016-03-17 [1] https://travis-ci.org/sybrenstuvel/python-rsa [2] https://coveralls.io/github/sybrenstuvel/python-rsa [3] https://codeclimate.com/github/sybrenstuvel/python-rsa -[4] http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pd +[4] http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf -Version 3.3 - released 2016-01-13 ----------------------------------------- +## Version 3.3 - released 2016-01-13 - Thanks to Filippo Valsorda: Fix BB'06 attack in verify() by switching from parsing to comparison. See [1] for more information. @@ -48,54 +86,46 @@ Version 3.3 - released 2016-01-13 [1] https://blog.filippo.io/bleichenbacher-06-signature-forgery-in-python-rsa/ -Version 3.2.3 - released 2015-11-05 ----------------------------------------- +## Version 3.2.3 - released 2015-11-05 - Added character encoding markers for Python 2.x -Version 3.2.1 - released 2015-11-05 ----------------------------------------- +## Version 3.2.1 - released 2015-11-05 - Added per-file licenses - Added support for wheel packages - Made example code more consistent and up to date with Python 3.4 -Version 3.2 - released 2015-07-29 ----------------------------------------- +## Version 3.2 - released 2015-07-29 - Mentioned support for Python 3 in setup.py -Version 3.1.4 - released 2014-02-22 ----------------------------------------- +## Version 3.1.4 - released 2014-02-22 - Fixed some bugs -Version 3.1.3 - released 2014-02-02 ----------------------------------------- +## Version 3.1.3 - released 2014-02-02 - Dropped support for Python 2.5 -Version 3.1.2 - released 2013-09-15 ----------------------------------------- +## Version 3.1.2 - released 2013-09-15 - Added Python 3.3 to the test environment. - Removed dependency on Distribute - Added support for loading public keys from OpenSSL -Version 3.1.1 - released 2012-06-18 ----------------------------------------- +## Version 3.1.1 - released 2012-06-18 - Fixed doctests for Python 2.7 - Removed obsolete unittest so all tests run fine on Python 3.2 -Version 3.1 - released 2012-06-17 ----------------------------------------- +## Version 3.1 - released 2012-06-17 - Big, big credits to Yesudeep Mangalapilly for all the changes listed below! @@ -108,35 +138,25 @@ Version 3.1 - released 2012-06-17 -Version 3.0.1 - released 2011-08-07 ----------------------------------------- +## Version 3.0.1 - released 2011-08-07 - Removed unused import of abc module -Version 3.0 - released 2011-08-05 ----------------------------------------- +## Version 3.0 - released 2011-08-05 - Changed the meaning of the keysize to mean the size of ``n`` rather than the size of both ``p`` and ``q``. This is the common interpretation of RSA keysize. To get the old behaviour, double the keysize when generating a new key. - - Added a lot of doctests - - Added random-padded encryption and decryption using PKCS#1 version 1.5 - - Added hash-based signatures and verification using PKCS#1v1.5 - - Modeling private and public key as real objects rather than dicts. - - Support for saving and loading keys as PEM and DER files. - - Ability to extract a public key from a private key (PEM+DER) -Version 2.0 ----------------------------------------- +## Version 2.0 - Security improvements by Barry Mead. - diff --git a/README.md b/README.md index ba1013b..ea24210 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,10 @@ Pure Python RSA implementation ============================== -[![PyPI](https://img.shields.io/pypi/v/rsa.svg)](https://pypi.python.org/pypi/rsa) -[![Build Status](https://travis-ci.org/sybrenstuvel/python-rsa.svg?branch=master)] - (https://travis-ci.org/sybrenstuvel/python-rsa) -[![Coverage Status](https://coveralls.io/repos/github/sybrenstuvel/python-rsa/badge.svg?branch=master)] - (https://coveralls.io/github/sybrenstuvel/python-rsa?branch=master) -[![Code Climate](https://img.shields.io/codeclimate/github/sybrenstuvel/python-rsa.svg)] - (https://codeclimate.com/github/sybrenstuvel/python-rsa) +[![PyPI](https://img.shields.io/pypi/v/rsa.svg)](https://pypi.org/project/rsa/) +[![Build Status](https://travis-ci.org/sybrenstuvel/python-rsa.svg?branch=master)](https://travis-ci.org/sybrenstuvel/python-rsa) +[![Coverage Status](https://coveralls.io/repos/github/sybrenstuvel/python-rsa/badge.svg?branch=master)](https://coveralls.io/github/sybrenstuvel/python-rsa?branch=master) +[![Code Climate](https://api.codeclimate.com/v1/badges/a99a88d28ad37a79dbf6/maintainability)](https://codeclimate.com/github/codeclimate/codeclimate/maintainability) [Python-RSA](https://stuvel.eu/rsa) is a pure-Python RSA implementation. It supports encryption and decryption, signing and verifying signatures, and key @@ -15,22 +12,27 @@ generation according to PKCS#1 version 1.5. It can be used as a Python library as well as on the commandline. The code was mostly written by Sybren A. Stüvel. -Documentation can be found at the [Python-RSA homepage](https://stuvel.eu/rsa). +Documentation can be found at the [Python-RSA homepage](https://stuvel.eu/rsa). For all changes, check [the changelog](https://github.com/sybrenstuvel/python-rsa/blob/master/CHANGELOG.md). Download and install using: pip install rsa -or download it from the [Python Package Index](https://pypi.python.org/pypi/rsa). +or download it from the [Python Package Index](https://pypi.org/project/rsa/). -The source code is maintained at [Github](https://github.com/sybrenstuvel/python-rsa/) and is +The source code is maintained at [GitHub](https://github.com/sybrenstuvel/python-rsa/) and is licensed under the [Apache License, version 2.0](https://www.apache.org/licenses/LICENSE-2.0) +Major changes in 4.1 +-------------------- + +Version 4.0 was the last version to support Python 2 and 3.4. Version 4.1 is compatible with Python 3.5+ only. -Plans for the future + +Major changes in 4.0 -------------------- -Version 3.4 is the last version in the 3.x range. Version 4.0 will drop the following modules, +Version 3.4 was the last version in the 3.x range. Version 4.0 drops the following modules, as they are insecure: - `rsa._version133` @@ -38,7 +40,9 @@ as they are insecure: - `rsa.bigfile` - `rsa.varblock` -Those modules are marked as deprecated in version 3.4. +Those modules were marked as deprecated in version 3.4. -Furthermore, in 4.0 the I/O functions will be streamlined to always work with bytes on all +Furthermore, in 4.0 the I/O functions is streamlined to always work with bytes on all supported versions of Python. + +Version 4.0 drops support for Python 2.6 and 3.3. diff --git a/create_timing_table.py b/create_timing_table.py index 6163916..498e6a0 100755 --- a/create_timing_table.py +++ b/create_timing_table.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/doc/cli.rst b/doc/cli.rst index af2b5f1..30864d7 100644 --- a/doc/cli.rst +++ b/doc/cli.rst @@ -13,31 +13,30 @@ on how to use them. Here is a short overview: .. index:: pyrsa-verify, pyrsa-priv2pub, pyrsa-encrypt-bigfile .. index:: pyrsa-decrypt-bigfile, pyrsa-decrypt-bigfile -+-----------------------+--------------------------------------------------+-----------------------------------------+ -| Command | Usage | Core function | -+=======================+==================================================+=========================================+ -| pyrsa-keygen | Generates a new RSA keypair in PEM or DER format | :py:func:`rsa.newkeys` | -+-----------------------+--------------------------------------------------+-----------------------------------------+ -| pyrsa-encrypt | Encrypts a file. The file must be shorter than | :py:func:`rsa.encrypt` | -| | the key length in order to be encrypted. | | -+-----------------------+--------------------------------------------------+-----------------------------------------+ -| pyrsa-decrypt | Decrypts a file. | :py:func:`rsa.decrypt` | -+-----------------------+--------------------------------------------------+-----------------------------------------+ -| pyrsa-sign | Signs a file, outputs the signature. | :py:func:`rsa.sign` | -+-----------------------+--------------------------------------------------+-----------------------------------------+ -| pyrsa-verify | Verifies a signature. The result is written to | :py:func:`rsa.verify` | -| | the console as well as returned in the exit | | -| | status code. | | -+-----------------------+--------------------------------------------------+-----------------------------------------+ -| pyrsa-priv2pub | Reads a private key and outputs the | \- | -| | corresponding public key. | | -+-----------------------+--------------------------------------------------+-----------------------------------------+ -| pyrsa-encrypt-bigfile | Encrypts a file to an encrypted VARBLOCK file. | :py:func:`rsa.bigfile.encrypt_bigfile` | -| | The file can be larger than the key length, but | | -| | the output file is only compatible with | | -| | Python-RSA. | | -+-----------------------+--------------------------------------------------+-----------------------------------------+ -| pyrsa-decrypt-bigfile | Decrypts an encrypted VARBLOCK file. | :py:func:`rsa.bigfile.encrypt_bigfile` | -+-----------------------+--------------------------------------------------+-----------------------------------------+ - - ++-------------------------+--------------------------------------------------+-----------------------------------------+ +| Command | Usage | Core function | ++=========================+==================================================+=========================================+ +| pyrsa-keygen | Generates a new RSA keypair in PEM or DER format | :py:func:`rsa.newkeys` | ++-------------------------+--------------------------------------------------+-----------------------------------------+ +| pyrsa-encrypt | Encrypts a file. The file must be shorter than | :py:func:`rsa.encrypt` | +| | the key length in order to be encrypted. | | ++-------------------------+--------------------------------------------------+-----------------------------------------+ +| pyrsa-decrypt | Decrypts a file. | :py:func:`rsa.decrypt` | ++-------------------------+--------------------------------------------------+-----------------------------------------+ +| pyrsa-sign | Signs a file, outputs the signature. | :py:func:`rsa.sign` | ++-------------------------+--------------------------------------------------+-----------------------------------------+ +| pyrsa-verify | Verifies a signature. The result is written to | :py:func:`rsa.verify` | +| | the console as well as returned in the exit | | +| | status code. | | ++-------------------------+--------------------------------------------------+-----------------------------------------+ +| pyrsa-priv2pub | Reads a private key and outputs the | \- | +| | corresponding public key. | | ++-------------------------+--------------------------------------------------+-----------------------------------------+ +| *pyrsa-encrypt-bigfile* | *Encrypts a file to an encrypted VARBLOCK file. | *Deprecated in Python-RSA 3.4 and | +| | The file can be larger than the key length, but | removed from version 4.0.* | +| | the output file is only compatible with | | +| | Python-RSA.* | | ++-------------------------+--------------------------------------------------+-----------------------------------------+ +| *pyrsa-decrypt-bigfile* | *Decrypts an encrypted VARBLOCK file.* | *Deprecated in Python-RSA 3.4 and | +| | | removed from version 4.0.* | ++-------------------------+--------------------------------------------------+-----------------------------------------+ diff --git a/doc/compatibility.rst b/doc/compatibility.rst index aedfcb6..1429553 100644 --- a/doc/compatibility.rst +++ b/doc/compatibility.rst @@ -16,7 +16,7 @@ Encryption: Signatures: PKCS#1 v1.5 using the following hash methods: - MD5, SHA-1, SHA-256, SHA-384, SHA-512 + MD5, SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-384, SHA3-512 Private keys: PKCS#1 v1.5 in PEM and DER format, ASN.1 type RSAPrivateKey @@ -25,7 +25,8 @@ Public keys: PKCS#1 v1.5 in PEM and DER format, ASN.1 type RSAPublicKey :ref:`VARBLOCK ` encryption: - Python-RSA only, not compatible with any other known application. + Deprecated in Python-RSA 3.4 and removed from Python-RSA 4.0. + Was Python-RSA only, not compatible with any other known application. .. _openssl: @@ -59,4 +60,3 @@ PKCS#8 format you need an external tool such as OpenSSL:: openssl rsa -in privatekey-pkcs8.pem -out privatekey.pem You can then extract the corresponding public key as described above. - diff --git a/doc/conf.py b/doc/conf.py index 95317b2..60201b1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Python-RSA documentation build configuration file, created by # sphinx-quickstart on Sat Jul 30 23:11:07 2011. # @@ -28,7 +26,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', - 'sphinx.ext.coverage', 'sphinx.ext.pngmath'] + 'sphinx.ext.coverage'] # I would like to add 'sphinx.ext.viewcode', but it causes a UnicodeDecodeError @@ -46,7 +44,7 @@ # General information about the project. project = u'Python-RSA' -copyright = u'2011-2016, Sybren A. Stüvel' +copyright = u'2011-2019, Sybren A. Stüvel' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/doc/installation.rst b/doc/installation.rst index 578dc86..00c46d8 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -1,27 +1,16 @@ Installation ============ -Installation can be done in various ways. The simplest form uses pip -or easy_install. Either one will work:: +Installation can be done in various ways. The simplest form uses pip:: pip install rsa - easy_install rsa - -Depending on your system you may need to use ``sudo pip`` or ``sudo -easy_install``. - -Installation from source is also quite easy. Download the source and -then type:: - - python setup.py install - -or if that doesn't work:: - - sudo python setup.py install +Depending on your system you may need to use ``sudo pip`` if you want to install +the library system-wide, or use ``pip install --user rsa`` to install the +library in your home directory. The sources are tracked in our `Git repository`_ at -Github. It also hosts the `issue tracker`_. +GitHub. It also hosts the `issue tracker`_. .. _`Git repository`: https://github.com/sybrenstuvel/python-rsa.git .. _`issue tracker`: https://github.com/sybrenstuvel/python-rsa/issues @@ -30,6 +19,9 @@ Github. It also hosts the `issue tracker`_. Dependencies ------------ +Python-RSA is compatible with Python versions 3.5 and newer. The last +version with Python 2.7 support was Python-RSA 4.0. + Python-RSA has very few dependencies. As a matter of fact, to use it you only need Python itself. Loading and saving keys does require an extra module, though: pyasn1. If you used pip or easy_install like @@ -39,17 +31,15 @@ described above, you should be ready to go. Development dependencies ------------------------ -In order to start developing on Python-RSA you need a bit more. Use -pip to install the development requirements in a virtual environment:: - - virtualenv -p /path/to/your-python-version python-rsa-venv - . python-rsa-venv/bin/activate - pip install -r python-rsa/requirements.txt +In order to start developing on Python-RSA, use Git_ to get a copy of +the source:: + git clone https://github.com/sybrenstuvel/python-rsa.git -Once these are installed, use Git_ to get a copy of the source:: +Use Poetry_ to install the development requirements in a virtual environment:: - hg clone https://github.com/sybrenstuvel/python-rsa.git - python setup.py develop + cd python-rsa + poetry install .. _Git: https://git-scm.com/ +.. _Poetry: https://poetry.eustace.io/ diff --git a/doc/reference.rst b/doc/reference.rst index d1b0b6d..9da7c6b 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -15,6 +15,8 @@ Functions .. autofunction:: rsa.verify +.. autofunction:: rsa.find_signature_hash + .. autofunction:: rsa.newkeys(keysize) @@ -49,32 +51,13 @@ Exceptions .. index:: VARBLOCK (file format) -Module: rsa.bigfile -------------------- - -.. warning:: - - The :py:mod:`rsa.bigfile` module is NOT recommended for general use, has been - deprecated since Python-RSA 3.4, and will be removed in a future release. It's - vulnerable to a number of attacks. See :ref:`bigfiles` for more information. - -The :py:mod:`rsa.bigfile` module contains functions for encrypting and -decrypting files that are larger than the RSA key. See -:ref:`bigfiles` for more information. - -.. autofunction:: rsa.bigfile.encrypt_bigfile - -.. autofunction:: rsa.bigfile.decrypt_bigfile - -.. _VARBLOCK: - The VARBLOCK file format ++++++++++++++++++++++++ .. warning:: The VARBLOCK format is NOT recommended for general use, has been deprecated since - Python-RSA 3.4, and will be removed in a future release. It's vulnerable to a + Python-RSA 3.4, and was removed in version 4.0. It's vulnerable to a number of attacks. See :ref:`bigfiles` for more information. The VARBLOCK file format allows us to encrypt files that are larger @@ -109,4 +92,3 @@ the core of the entire library. .. autofunction:: rsa.core.encrypt_int .. autofunction:: rsa.core.decrypt_int - diff --git a/doc/upgrading.rst b/doc/upgrading.rst index 0ec18eb..3381baa 100644 --- a/doc/upgrading.rst +++ b/doc/upgrading.rst @@ -1,6 +1,22 @@ Upgrading from older versions ============================= +From versions older than Python-RSA 4.0 +--------------------------------------- + +Support for the VARBLOCK/bigfile format has been dropped in version 4.0, after +being deprecated for a year. There is no alternative implementation in +Python-RSA 4.0. If you need this, or have ideas on how to do handle encryption +of large files securely and in a compatible way with existing standards, +`open a ticket to discuss this`_. + +.. _open a ticket to discuss this: + https://github.com/sybrenstuvel/python-rsa/issues/new + + +From versions older than Python-RSA 3.4 +--------------------------------------- + Previous versions of Python-RSA were less secure than the current version. In order to be able to gradually upgrade your software, those old versions will be available until Python-RSA 4.0. @@ -34,8 +50,7 @@ less secure code into your project. The random padding introduced in version 3.0 made things much more secure, but also requires a larger key to encrypt the same message. -You can either generate a new key with :py:func:`rsa.newkeys`, or use -:py:func:`rsa.bigfile.encrypt_bigfile` to encrypt your files. + Converting keys --------------- @@ -70,4 +85,3 @@ older version of Python-RSA, use the following:: old_priv_key.update(old_pub_key) priv_key = rsa.PrivateKey(**old_priv_key) - diff --git a/doc/usage.rst b/doc/usage.rst index a3d128d..b1244d4 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -13,13 +13,13 @@ and a public key. The private key is called *private* for a reason. Never share this key with anyone. -The public key is used for encypting a message such that it can only +The public key is used for encrypting a message such that it can only be read by the owner of the private key. As such it's also referred to as the *encryption key*. Decrypting a message can only be done using the private key, hence it's also called the *decryption key*. The private key is used for signing a message. With this signature and -the public key, the receiver can verifying that a message was signed +the public key, the receiver can verify that a message was signed by the owner of the private key, and that the message was not modified after signing. @@ -90,32 +90,6 @@ generate them for you, then load them in your Python code. OpenSSL generates a 4096-bit key in 3.5 seconds on the same machine as used above. See :ref:`openssl` for more information. -Key size requirements ---------------------- - -Python-RSA version 3.0 introduced PKCS#1-style random padding. This -means that 11 bytes (88 bits) of your key are no longer usable for -encryption, so keys smaller than this are unusable. The larger the -key, the higher the security. - -Creating signatures also requires a key of a certain size, depending -on the used hash method: - -+-------------+-----------------------------------+ -| Hash method | Suggested minimum key size (bits) | -+=============+===================================+ -| MD5 | 360 | -+-------------+-----------------------------------+ -| SHA-1 | 368 | -+-------------+-----------------------------------+ -| SHA-256 | 496 | -+-------------+-----------------------------------+ -| SHA-384 | 624 | -+-------------+-----------------------------------+ -| SHA-512 | 752 | -+-------------+-----------------------------------+ - - Encryption and decryption ------------------------- @@ -198,11 +172,20 @@ You can create a detached signature for a message using the >>> (pubkey, privkey) = rsa.newkeys(512) >>> message = 'Go left at the blue tree' >>> signature = rsa.sign(message, privkey, 'SHA-1') - + This hashes the message using SHA-1. Other hash methods are also possible, check the :py:func:`rsa.sign` function documentation for details. The hash is then signed with the private key. +It is possible to calculate the hash and signature in separate operations +(i.e for generating the hash on a client machine and then sign with a +private key on remote server). To hash a message use the :py:func:`rsa.compute_hash` +function and then use the :py:func:`rsa.sign_hash` function to sign the hash: + + >>> message = 'Go left at the blue tree' + >>> hash = rsa.compute_hash(message, 'SHA-1') + >>> signature = rsa.sign_hash(hash, privkey, 'SHA-1') + In order to verify the signature, use the :py:func:`rsa.verify` function. This function returns True if the verification is successful: @@ -285,7 +268,7 @@ Only using Python-RSA: the VARBLOCK format .. warning:: The VARBLOCK format is NOT recommended for general use, has been deprecated since - Python-RSA 3.4, and will be removed in a future release. It's vulnerable to a + Python-RSA 3.4, and has been removed in version 4.0. It's vulnerable to a number of attacks: 1. decrypt/encrypt_bigfile() does not implement `Authenticated encryption`_ nor @@ -294,60 +277,11 @@ Only using Python-RSA: the VARBLOCK format 2. decrypt/encrypt_bigfile() does not use hybrid encryption (it uses plain RSA) and has no method for chaining, so block reordering is possible. - See `issue #19 on Github`_ for more information. + See `issue #19 on GitHub`_ for more information. .. _Authenticated encryption: https://en.wikipedia.org/wiki/Authenticated_encryption -.. _issue #19 on Github: https://github.com/sybrenstuvel/python-rsa/issues/13 - - -As far as we know, there is no pure-Python AES encryption. Previous -versions of Python-RSA included functionality to encrypt large files -with just RSA, and so does this version. The format has been improved, -though. - -Encrypting works as follows: the input file is split into blocks that -are just large enough to encrypt with your RSA key. Every block is -then encrypted using RSA, and the encrypted blocks are assembled into -the output file. This file format is called the :ref:`VARBLOCK -` format. - -Decrypting works in reverse. The encrypted file is separated into -encrypted blocks. Those are decrypted, and assembled into the original -file. - -.. note:: - - The file will get larger after encryption, as each encrypted block - has 8 bytes of random padding and 3 more bytes of overhead. - -Since these encryption/decryption functions are potentially called on -very large files, they use another approach. Where the regular -functions store the message in memory in its entirety, these functions -work on one block at the time. As a result, you should call them with -:py:class:`file`-like objects as the parameters. - -Before using we of course need a keypair: - ->>> import rsa ->>> (pub_key, priv_key) = rsa.newkeys(512) - -Encryption works on file handles using the -:py:func:`rsa.bigfile.encrypt_bigfile` function: - ->>> from rsa.bigfile import * ->>> with open('inputfile', 'rb') as infile, open('outputfile', 'wb') as outfile: -... encrypt_bigfile(infile, outfile, pub_key) - -As does decryption using the :py:func:`rsa.bigfile.decrypt_bigfile` -function: - ->>> from rsa.bigfile import * ->>> with open('inputfile', 'rb') as infile, open('outputfile', 'wb') as outfile: -... decrypt_bigfile(infile, outfile, priv_key) - -.. note:: - - :py:func:`rsa.sign` and :py:func:`rsa.verify` work on arbitrarily - long files, so they do not have a "bigfile" equivalent. - +.. _issue #19 on GitHub: https://github.com/sybrenstuvel/python-rsa/issues/13 +As of Python-RSA version 4.0, the VARBLOCK format has been removed from the +library. For now, this section is kept here to document the issues with that +format, and ensure we don't do something like that again. diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..173c007 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1008 @@ +[[package]] +category = "dev" +description = "A configurable sidebar-enabled Sphinx theme" +name = "alabaster" +optional = false +python-versions = "*" +version = "0.7.12" + +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.4" + +[[package]] +category = "dev" +description = "Atomic file writes." +marker = "sys_platform == \"win32\"" +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "dev" +description = "Internationalization utilities" +name = "babel" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8.0" + +[package.dependencies] +pytz = ">=2015.7" + +[[package]] +category = "dev" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2020.4.5.2" + +[[package]] +category = "dev" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "Code coverage measurement for Python" +name = "coverage" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "5.1" + +[package.extras] +toml = ["toml"] + +[[package]] +category = "dev" +description = "Show coverage stats online via coveralls.io" +name = "coveralls" +optional = false +python-versions = "*" +version = "1.11.1" + +[package.dependencies] +coverage = ">=3.6,<6.0" +docopt = ">=0.6.1" +requests = ">=1.0.0" + +[package.extras] +yaml = ["PyYAML (>=3.10,<5.3)"] + +[[package]] +category = "dev" +description = "Distribution utilities" +name = "distlib" +optional = false +python-versions = "*" +version = "0.3.0" + +[[package]] +category = "dev" +description = "Pythonic argument parser, that will make you smile" +name = "docopt" +optional = false +python-versions = "*" +version = "0.6.2" + +[[package]] +category = "dev" +description = "Docutils -- Python Documentation Utilities" +name = "docutils" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.16" + +[[package]] +category = "dev" +description = "A platform independent file lock." +name = "filelock" +optional = false +python-versions = "*" +version = "3.0.12" + +[[package]] +category = "dev" +description = "the modular source code checker: pep8 pyflakes and co" +name = "flake8" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "3.8.3" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + +[[package]] +category = "dev" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.9" + +[[package]] +category = "dev" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +name = "imagesize" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.2.0" + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.6.1" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] + +[[package]] +category = "dev" +description = "Read resources from Python packages" +marker = "python_version < \"3.7\"" +name = "importlib-resources" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.5.0" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + +[package.dependencies.zipp] +python = "<3.8" +version = ">=0.4" + +[package.extras] +docs = ["sphinx", "rst.linker", "jaraco.packaging"] + +[[package]] +category = "dev" +description = "A very fast and expressive template engine." +name = "jinja2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.11.2" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +category = "dev" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.3.0" + +[[package]] +category = "dev" +description = "Optional static typing for Python" +name = "mypy" +optional = false +python-versions = "*" +version = "0.720" + +[package.dependencies] +mypy-extensions = ">=0.4.0,<0.5.0" +typed-ast = ">=1.4.0,<1.5.0" +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] + +[[package]] +category = "dev" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +name = "mypy-extensions" +optional = false +python-versions = "*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.4" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "dev" +description = "Object-oriented filesystem paths" +marker = "python_version >= \"3.5\" and python_version < \"3.6\" or python_version < \"3.6\"" +name = "pathlib2" +optional = false +python-versions = "*" +version = "2.3.5" + +[package.dependencies] +six = "*" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.1" + +[[package]] +category = "main" +description = "ASN.1 types and codecs" +name = "pyasn1" +optional = false +python-versions = "*" +version = "0.4.8" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.6.0" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.2.0" + +[[package]] +category = "dev" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=3.5" +version = "2.6.1" + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + +[[package]] +category = "main" +description = "SHA-3 (Keccak) for Python 2.7 - 3.5" +marker = "python_version >= \"3.5\" and python_version < \"3.6\"" +name = "pysha3" +optional = false +python-versions = "*" +version = "1.0.2" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "5.4.3" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.dependencies.pathlib2] +python = "<3.6" +version = ">=2.2.0" + +[package.extras] +checkqa-mypy = ["mypy (v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "dev" +description = "Pytest plugin for measuring coverage." +name = "pytest-cov" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.9.0" + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=3.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] + +[[package]] +category = "dev" +description = "World timezone definitions, modern and historical" +name = "pytz" +optional = false +python-versions = "*" +version = "2020.1" + +[[package]] +category = "dev" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.23.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "dev" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + +[[package]] +category = "dev" +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +name = "snowballstemmer" +optional = false +python-versions = "*" +version = "2.0.0" + +[[package]] +category = "dev" +description = "Python documentation generator" +name = "sphinx" +optional = false +python-versions = ">=3.5" +version = "2.4.4" + +[package.dependencies] +Jinja2 = ">=2.3" +Pygments = ">=2.0" +alabaster = ">=0.7,<0.8" +babel = ">=1.3,<2.0 || >2.0" +colorama = ">=0.3.5" +docutils = ">=0.12" +imagesize = "*" +packaging = "*" +requests = ">=2.5.0" +setuptools = "*" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = "*" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = "*" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +test = ["pytest (<5.3.3)", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.761)", "docutils-stubs"] + +[[package]] +category = "dev" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +name = "sphinxcontrib-applehelp" +optional = false +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +name = "sphinxcontrib-devhelp" +optional = false +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +name = "sphinxcontrib-htmlhelp" +optional = false +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +category = "dev" +description = "A sphinx extension which renders display math in HTML via JavaScript" +name = "sphinxcontrib-jsmath" +optional = false +python-versions = ">=3.5" +version = "1.0.1" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +category = "dev" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +name = "sphinxcontrib-qthelp" +optional = false +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +name = "sphinxcontrib-serializinghtml" +optional = false +python-versions = ">=3.5" +version = "1.1.4" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.1" + +[[package]] +category = "dev" +description = "tox is a generic virtualenv management and test command line tool" +name = "tox" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "3.15.2" + +[package.dependencies] +colorama = ">=0.4.1" +filelock = ">=3.0.0" +packaging = ">=14" +pluggy = ">=0.12.0" +py = ">=1.4.17" +six = ">=1.14.0" +toml = ">=0.9.4" +virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12,<2" + +[package.extras] +docs = ["sphinx (>=2.0.0)", "towncrier (>=18.5.0)", "pygments-github-lexers (>=0.0.5)", "sphinxcontrib-autoprogram (>=0.1.5)"] +testing = ["freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-xdist (>=1.22.2)", "pytest-randomly (>=1.0.0)", "flaky (>=3.4.0)", "psutil (>=5.6.1)"] + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.1" + +[[package]] +category = "dev" +description = "Backported and Experimental Type Hints for Python 3.5+" +name = "typing-extensions" +optional = false +python-versions = "*" +version = "3.7.4.2" + +[[package]] +category = "dev" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.9" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +category = "dev" +description = "Virtual Python Environment builder" +name = "virtualenv" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "20.0.21" + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.0,<1" +filelock = ">=3.0.0,<4" +six = ">=1.9.0,<2" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12,<2" + +[package.dependencies.importlib-resources] +python = "<3.7" +version = ">=1.0,<2" + +[package.extras] +docs = ["sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2)"] +testing = ["pytest (>=4)", "coverage (>=5)", "coverage-enable-subprocess (>=1)", "pytest-xdist (>=1.31.0)", "pytest-mock (>=2)", "pytest-env (>=0.6.2)", "pytest-randomly (>=1)", "pytest-timeout", "packaging (>=20.0)", "xonsh (>=0.9.16)"] + +[[package]] +category = "dev" +description = "Measures the displayed width of unicode strings in a terminal" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.2.4" + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=2.7" +version = "1.2.0" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] + +[metadata] +content-hash = "d9a51bfd7b1f873f2cc17e37a64cf0e03eefcc3410f741dff2a45a01bb4a2b3b" +python-versions = "^3.5" + +[metadata.files] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +babel = [ + {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, + {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, +] +certifi = [ + {file = "certifi-2020.4.5.2-py2.py3-none-any.whl", hash = "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"}, + {file = "certifi-2020.4.5.2.tar.gz", hash = "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +coverage = [ + {file = "coverage-5.1-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65"}, + {file = "coverage-5.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2"}, + {file = "coverage-5.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04"}, + {file = "coverage-5.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6"}, + {file = "coverage-5.1-cp27-cp27m-win32.whl", hash = "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796"}, + {file = "coverage-5.1-cp27-cp27m-win_amd64.whl", hash = "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730"}, + {file = "coverage-5.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0"}, + {file = "coverage-5.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a"}, + {file = "coverage-5.1-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf"}, + {file = "coverage-5.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9"}, + {file = "coverage-5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768"}, + {file = "coverage-5.1-cp35-cp35m-win32.whl", hash = "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2"}, + {file = "coverage-5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7"}, + {file = "coverage-5.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0"}, + {file = "coverage-5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019"}, + {file = "coverage-5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c"}, + {file = "coverage-5.1-cp36-cp36m-win32.whl", hash = "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1"}, + {file = "coverage-5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7"}, + {file = "coverage-5.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355"}, + {file = "coverage-5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489"}, + {file = "coverage-5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd"}, + {file = "coverage-5.1-cp37-cp37m-win32.whl", hash = "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e"}, + {file = "coverage-5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a"}, + {file = "coverage-5.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55"}, + {file = "coverage-5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c"}, + {file = "coverage-5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef"}, + {file = "coverage-5.1-cp38-cp38-win32.whl", hash = "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24"}, + {file = "coverage-5.1-cp38-cp38-win_amd64.whl", hash = "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0"}, + {file = "coverage-5.1-cp39-cp39-win32.whl", hash = "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4"}, + {file = "coverage-5.1-cp39-cp39-win_amd64.whl", hash = "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e"}, + {file = "coverage-5.1.tar.gz", hash = "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"}, +] +coveralls = [ + {file = "coveralls-1.11.1-py2.py3-none-any.whl", hash = "sha256:4b6bfc2a2a77b890f556bc631e35ba1ac21193c356393b66c84465c06218e135"}, + {file = "coveralls-1.11.1.tar.gz", hash = "sha256:67188c7ec630c5f708c31552f2bcdac4580e172219897c4136504f14b823132f"}, +] +distlib = [ + {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, +] +docopt = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] +docutils = [ + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] +flake8 = [ + {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, + {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, +] +idna = [ + {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, + {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, +] +imagesize = [ + {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, + {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.6.1-py2.py3-none-any.whl", hash = "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"}, + {file = "importlib_metadata-1.6.1.tar.gz", hash = "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545"}, +] +importlib-resources = [ + {file = "importlib_resources-1.5.0-py2.py3-none-any.whl", hash = "sha256:85dc0b9b325ff78c8bef2e4ff42616094e16b98ebd5e3b50fe7e2f0bbcdcde49"}, + {file = "importlib_resources-1.5.0.tar.gz", hash = "sha256:6f87df66833e1942667108628ec48900e02a4ab4ad850e25fbf07cb17cf734ca"}, +] +jinja2 = [ + {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, + {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +more-itertools = [ + {file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"}, + {file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"}, +] +mypy = [ + {file = "mypy-0.720-cp35-cp35m-macosx_10_6_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:437020a39417e85e22ea8edcb709612903a9924209e10b3ec6d8c9f05b79f498"}, + {file = "mypy-0.720-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:10af62f87b6921eac50271e667cc234162a194e742d8e02fc4ddc121e129a5b0"}, + {file = "mypy-0.720-cp35-cp35m-win_amd64.whl", hash = "sha256:0107bff4f46a289f0e4081d59b77cef1c48ea43da5a0dbf0005d54748b26df2a"}, + {file = "mypy-0.720-cp36-cp36m-macosx_10_6_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:352c24ba054a89bb9a35dd064ee95ab9b12903b56c72a8d3863d882e2632dc76"}, + {file = "mypy-0.720-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7a17613f7ea374ab64f39f03257f22b5755335b73251d0d253687a69029701ba"}, + {file = "mypy-0.720-cp36-cp36m-win_amd64.whl", hash = "sha256:07957f5471b3bb768c61f08690c96d8a09be0912185a27a68700f3ede99184e4"}, + {file = "mypy-0.720-cp37-cp37m-macosx_10_6_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:6724fcd5777aa6cebfa7e644c526888c9d639bd22edd26b2a8038c674a7c34bd"}, + {file = "mypy-0.720-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:15e43d3b1546813669bd1a6ec7e6a11d2888db938e0607f7b5eef6b976671339"}, + {file = "mypy-0.720-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc1151ced496ca1496272da7fc356580e95f2682be1d32377c22ddebdf73c91"}, + {file = "mypy-0.720-py3-none-any.whl", hash = "sha256:11fd60d2f69f0cefbe53ce551acf5b1cec1a89e7ce2d47b4e95a84eefb2899ae"}, + {file = "mypy-0.720.tar.gz", hash = "sha256:49925f9da7cee47eebf3420d7c0e00ec662ec6abb2780eb0a16260a7ba25f9c4"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] +pathlib2 = [ + {file = "pathlib2-2.3.5-py2.py3-none-any.whl", hash = "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db"}, + {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, + {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, +] +pyasn1 = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] +pycodestyle = [ + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] +pygments = [ + {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, + {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pysha3 = [ + {file = "pysha3-1.0.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:6e6a84efb7856f5d760ee55cd2b446972cb7b835676065f6c4f694913ea8f8d9"}, + {file = "pysha3-1.0.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f9046d59b3e72aa84f6dae83a040bd1184ebd7fef4e822d38186a8158c89e3cf"}, + {file = "pysha3-1.0.2-cp27-cp27m-win32.whl", hash = "sha256:9fdd28884c5d0b4edfed269b12badfa07f1c89dbc5c9c66dd279833894a9896b"}, + {file = "pysha3-1.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:41be70b06c8775a9e4d4eeb52f2f6a3f356f17539a54eac61f43a29e42fd453d"}, + {file = "pysha3-1.0.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:68c3a60a39f9179b263d29e221c1bd6e01353178b14323c39cc70593c30f21c5"}, + {file = "pysha3-1.0.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:59111c08b8f34495575d12e5f2ce3bafb98bea470bc81e70c8b6df99aef0dd2f"}, + {file = "pysha3-1.0.2-cp33-cp33m-win32.whl", hash = "sha256:571a246308a7b63f15f5aa9651f99cf30f2a6acba18eddf28f1510935968b603"}, + {file = "pysha3-1.0.2-cp33-cp33m-win_amd64.whl", hash = "sha256:93abd775dac570cb9951c4e423bcb2bc6303a9d1dc0dc2b7afa2dd401d195b24"}, + {file = "pysha3-1.0.2-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:11a2ba7a2e1d9669d0052fc8fb30f5661caed5512586ecbeeaf6bf9478ab5c48"}, + {file = "pysha3-1.0.2-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:5ec8da7c5c70a53b5fa99094af3ba8d343955b212bc346a0d25f6ff75853999f"}, + {file = "pysha3-1.0.2-cp34-cp34m-win32.whl", hash = "sha256:9c778fa8b161dc9348dc5cc361e94d54aa5ff18413788f4641f6600d4893a608"}, + {file = "pysha3-1.0.2-cp34-cp34m-win_amd64.whl", hash = "sha256:fd7e66999060d079e9c0e8893e78d8017dad4f59721f6fe0be6307cd32127a07"}, + {file = "pysha3-1.0.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:827b308dc025efe9b6b7bae36c2e09ed0118a81f792d888548188e97b9bf9a3d"}, + {file = "pysha3-1.0.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:4416f16b0f1605c25f627966f76873e432971824778b369bd9ce1bb63d6566d9"}, + {file = "pysha3-1.0.2-cp35-cp35m-win32.whl", hash = "sha256:c93a2676e6588abcfaecb73eb14485c81c63b94fca2000a811a7b4fb5937b8e8"}, + {file = "pysha3-1.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:684cb01d87ed6ff466c135f1c83e7e4042d0fc668fa20619f581e6add1d38d77"}, + {file = "pysha3-1.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:386998ee83e313b6911327174e088021f9f2061cbfa1651b97629b761e9ef5c4"}, + {file = "pysha3-1.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c7c2adcc43836223680ebdf91f1d3373543dc32747c182c8ca2e02d1b69ce030"}, + {file = "pysha3-1.0.2-cp36-cp36m-win32.whl", hash = "sha256:cd5c961b603bd2e6c2b5ef9976f3238a561c58569945d4165efb9b9383b050ef"}, + {file = "pysha3-1.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:0060a66be16665d90c432f55a0ba1f6480590cfb7d2ad389e688a399183474f0"}, + {file = "pysha3-1.0.2.tar.gz", hash = "sha256:fe988e73f2ce6d947220624f04d467faf05f1bbdbc64b0a201296bb3af92739e"}, +] +pytest = [ + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, +] +pytest-cov = [ + {file = "pytest-cov-2.9.0.tar.gz", hash = "sha256:b6a814b8ed6247bd81ff47f038511b57fe1ce7f4cc25b9106f1a4b106f1d9322"}, + {file = "pytest_cov-2.9.0-py2.py3-none-any.whl", hash = "sha256:c87dfd8465d865655a8213859f1b4749b43448b5fae465cb981e16d52a811424"}, +] +pytz = [ + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, +] +requests = [ + {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, + {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, + {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, +] +sphinx = [ + {file = "Sphinx-2.4.4-py3-none-any.whl", hash = "sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb"}, + {file = "Sphinx-2.4.4.tar.gz", hash = "sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"}, + {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, + {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, +] +toml = [ + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, +] +tox = [ + {file = "tox-3.15.2-py2.py3-none-any.whl", hash = "sha256:50a188b8e17580c1fb931f494a754e6507d4185f54fb18aca5ba3e12d2ffd55e"}, + {file = "tox-3.15.2.tar.gz", hash = "sha256:c696d36cd7c6a28ada2da780400e44851b20ee19ef08cfe73344a1dcebbbe9f3"}, +] +typed-ast = [ + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, + {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, + {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, +] +urllib3 = [ + {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, + {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, +] +virtualenv = [ + {file = "virtualenv-20.0.21-py2.py3-none-any.whl", hash = "sha256:a730548b27366c5e6cbdf6f97406d861cccece2e22275e8e1a757aeff5e00c70"}, + {file = "virtualenv-20.0.21.tar.gz", hash = "sha256:a116629d4e7f4d03433b8afa27f43deba09d48bc48f5ecefa4f015a178efb6cf"}, +] +wcwidth = [ + {file = "wcwidth-0.2.4-py2.py3-none-any.whl", hash = "sha256:79375666b9954d4a1a10739315816324c3e73110af9d0e102d906fdb0aec009f"}, + {file = "wcwidth-0.2.4.tar.gz", hash = "sha256:8c6b5b6ee1360b842645f336d9e5d68c55817c26d3050f46b235ef2bc650e48f"}, +] +zipp = [ + {file = "zipp-1.2.0-py2.py3-none-any.whl", hash = "sha256:e0d9e63797e483a30d27e09fffd308c59a700d365ec34e93cc100844168bf921"}, + {file = "zipp-1.2.0.tar.gz", hash = "sha256:c70410551488251b0fee67b460fb9a536af8d6f9f008ad10ac51f615b6a521b1"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1f85c06 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[tool.poetry] +name = "python-rsa" +version = "4.1" +license = "Apache-2.0" +description = "Pure-Python RSA implementation" +authors = ["Sybren A. Stüvel "] +homepage = "https://stuvel.eu/rsa" +documentation = "https://stuvel.eu/python-rsa-doc/" +repository = "https://github.com/sybrenstuvel/python-rsa/" +classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Intended Audience :: Education', + 'Intended Audience :: Information Technology', + 'Operating System :: OS Independent', + 'Topic :: Security :: Cryptography', +] + +packages = [ + { include="rsa", from="." }, +] + +[tool.poetry.scripts] +pyrsa-decrypt = "rsa.cli:decrypt" +pyrsa-encrypt = "rsa.cli:encrypt" +pyrsa-keygen = "rsa.cli:keygen" +pyrsa-priv2pub = "rsa.util:private_to_public" +pyrsa-sign = "rsa.cli:sign" +pyrsa-verify = "rsa.cli:verify" + + +[tool.poetry.dependencies] +python = "^3.5" +pyasn1 = ">=0.1.3" +pysha3 = {version="^1.0", python="~3.5"} + +[tool.poetry.dev-dependencies] +coveralls = "^1.8" +Sphinx = "^2.1" +pathlib2 = {version="^2.3.4", python="~3.5"} +pytest = "^5.0" +pytest-cov = "^2.7" +tox = "^3.13" +mypy = "^0.720" +flake8 = "^3.7" + + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f1c6af1..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -sphinx -pyasn1>=0.1.3 -tox -wheel diff --git a/rsa/__init__.py b/rsa/__init__.py index c572c06..a0b482c 100644 --- a/rsa/__init__.py +++ b/rsa/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,19 +16,18 @@ Module for calculating large primes, and RSA encryption, decryption, signing and verification. Includes generating public and private keys. -WARNING: this implementation does not use random padding, compression of the -cleartext input to prevent repetitions, or other common security improvements. -Use with care. +WARNING: this implementation does not use compression of the cleartext input to +prevent repetitions, or other common security improvements. Use with care. """ from rsa.key import newkeys, PrivateKey, PublicKey from rsa.pkcs1 import encrypt, decrypt, sign, verify, DecryptionError, \ - VerificationError + VerificationError, find_signature_hash, sign_hash, compute_hash __author__ = "Sybren Stuvel, Barry Mead and Yesudeep Mangalapilly" -__date__ = "2016-03-29" -__version__ = '3.4.2' +__date__ = '2020-06-10' +__version__ = '4.1' # Do doctest if we're run directly if __name__ == "__main__": @@ -39,4 +36,5 @@ doctest.testmod() __all__ = ["newkeys", "encrypt", "decrypt", "sign", "verify", 'PublicKey', - 'PrivateKey', 'DecryptionError', 'VerificationError'] + 'PrivateKey', 'DecryptionError', 'VerificationError', + 'find_signature_hash', 'compute_hash', 'sign_hash'] diff --git a/rsa/_compat.py b/rsa/_compat.py index 93393d9..050e81b 100644 --- a/rsa/_compat.py +++ b/rsa/_compat.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,91 +14,14 @@ """Python compatibility wrappers.""" -from __future__ import absolute_import - -import sys from struct import pack -try: - MAX_INT = sys.maxsize -except AttributeError: - MAX_INT = sys.maxint - -MAX_INT64 = (1 << 63) - 1 -MAX_INT32 = (1 << 31) - 1 -MAX_INT16 = (1 << 15) - 1 - -# Determine the word size of the processor. -if MAX_INT == MAX_INT64: - # 64-bit processor. - MACHINE_WORD_SIZE = 64 -elif MAX_INT == MAX_INT32: - # 32-bit processor. - MACHINE_WORD_SIZE = 32 -else: - # Else we just assume 64-bit processor keeping up with modern times. - MACHINE_WORD_SIZE = 64 - -try: - # < Python3 - unicode_type = unicode -except NameError: - # Python3. - unicode_type = str - -# Fake byte literals. -if str is unicode_type: - def byte_literal(s): - return s.encode('latin1') -else: - def byte_literal(s): - return s - -# ``long`` is no more. Do type detection using this instead. -try: - integer_types = (int, long) -except NameError: - integer_types = (int,) -b = byte_literal - -# To avoid calling b() multiple times in tight loops. -ZERO_BYTE = b('\x00') -EMPTY_BYTE = b('') - - -def is_bytes(obj): - """ - Determines whether the given value is a byte string. - - :param obj: - The value to test. - :returns: - ``True`` if ``value`` is a byte string; ``False`` otherwise. - """ - return isinstance(obj, bytes) - - -def is_integer(obj): - """ - Determines whether the given value is an integer. - - :param obj: - The value to test. - :returns: - ``True`` if ``value`` is an integer; ``False`` otherwise. - """ - return isinstance(obj, integer_types) - - -def byte(num): +def byte(num: int) -> bytes: """ Converts a number between 0 and 255 (both inclusive) to a base-256 (byte) representation. - Use it as a replacement for ``chr`` where you are expecting a byte - because this will work on all current versions of Python:: - :param num: An unsigned integer between 0 and 255 (both inclusive). :returns: @@ -109,40 +30,19 @@ def byte(num): return pack("B", num) -def get_word_alignment(num, force_arch=64, - _machine_word_size=MACHINE_WORD_SIZE): +def xor_bytes(b1: bytes, b2: bytes) -> bytes: """ - Returns alignment details for the given number based on the platform - Python is running on. + Returns the bitwise XOR result between two bytes objects, b1 ^ b2. - :param num: - Unsigned integral number. - :param force_arch: - If you don't want to use 64-bit unsigned chunks, set this to - anything other than 64. 32-bit chunks will be preferred then. - Default 64 will be used when on a 64-bit machine. - :param _machine_word_size: - (Internal) The machine word size used for alignment. - :returns: - 4-tuple:: + Bitwise XOR operation is commutative, so order of parameters doesn't + generate different results. If parameters have different length, extra + length of the largest one is ignored. - (word_bits, word_bytes, - max_uint, packing_format_type) + :param b1: + First bytes object. + :param b2: + Second bytes object. + :returns: + Bytes object, result of XOR operation. """ - max_uint64 = 0xffffffffffffffff - max_uint32 = 0xffffffff - max_uint16 = 0xffff - max_uint8 = 0xff - - if force_arch == 64 and _machine_word_size >= 64 and num > max_uint32: - # 64-bit unsigned integer. - return 64, 8, max_uint64, "Q" - elif num > max_uint16: - # 32-bit unsigned integer - return 32, 4, max_uint32, "L" - elif num > max_uint8: - # 16-bit unsigned integer. - return 16, 2, max_uint16, "H" - else: - # 8-bit unsigned integer. - return 8, 1, max_uint8, "B" + return bytes(x ^ y for x, y in zip(b1, b2)) diff --git a/rsa/_version133.py b/rsa/_version133.py deleted file mode 100644 index ff03b45..0000000 --- a/rsa/_version133.py +++ /dev/null @@ -1,441 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2011 Sybren A. Stüvel -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Deprecated version of the RSA module - -.. deprecated:: 2.0 - - This submodule is deprecated and will be completely removed as of version 4.0. - -Module for calculating large primes, and RSA encryption, decryption, -signing and verification. Includes generating public and private keys. - -WARNING: this code implements the mathematics of RSA. It is not suitable for -real-world secure cryptography purposes. It has not been reviewed by a security -expert. It does not include padding of data. There are many ways in which the -output of this module, when used without any modification, can be sucessfully -attacked. -""" - -__author__ = "Sybren Stuvel, Marloes de Boer and Ivo Tamboer" -__date__ = "2010-02-05" -__version__ = '1.3.3' - -# NOTE: Python's modulo can return negative numbers. We compensate for -# this behaviour using the abs() function - -try: - import cPickle as pickle -except ImportError: - import pickle -from pickle import dumps, loads -import base64 -import math -import os -import random -import sys -import types -import zlib - -from rsa._compat import byte - -# Display a warning that this insecure version is imported. -import warnings -warnings.warn('Insecure version of the RSA module is imported as %s, be careful' - % __name__) -warnings.warn('This submodule is deprecated and will be completely removed as of version 4.0.', - DeprecationWarning) - - -def gcd(p, q): - """Returns the greatest common divisor of p and q - - - >>> gcd(42, 6) - 6 - """ - if p 0: - string = "%s%s" % (byte(number & 0xFF), string) - number /= 256 - - return string - -def fast_exponentiation(a, p, n): - """Calculates r = a^p mod n - """ - result = a % n - remainders = [] - while p != 1: - remainders.append(p & 1) - p = p >> 1 - while remainders: - rem = remainders.pop() - result = ((a ** rem) * result ** 2) % n - return result - -def read_random_int(nbits): - """Reads a random integer of approximately nbits bits rounded up - to whole bytes""" - - nbytes = ceil(nbits/8.) - randomdata = os.urandom(nbytes) - return bytes2int(randomdata) - -def ceil(x): - """ceil(x) -> int(math.ceil(x))""" - - return int(math.ceil(x)) - -def randint(minvalue, maxvalue): - """Returns a random integer x with minvalue <= x <= maxvalue""" - - # Safety - get a lot of random data even if the range is fairly - # small - min_nbits = 32 - - # The range of the random numbers we need to generate - range = maxvalue - minvalue - - # Which is this number of bytes - rangebytes = ceil(math.log(range, 2) / 8.) - - # Convert to bits, but make sure it's always at least min_nbits*2 - rangebits = max(rangebytes * 8, min_nbits * 2) - - # Take a random number of bits between min_nbits and rangebits - nbits = random.randint(min_nbits, rangebits) - - return (read_random_int(nbits) % range) + minvalue - -def fermat_little_theorem(p): - """Returns 1 if p may be prime, and something else if p definitely - is not prime""" - - a = randint(1, p-1) - return fast_exponentiation(a, p-1, p) - -def jacobi(a, b): - """Calculates the value of the Jacobi symbol (a/b) - """ - - if a % b == 0: - return 0 - result = 1 - while a > 1: - if a & 1: - if ((a-1)*(b-1) >> 2) & 1: - result = -result - b, a = a, b % a - else: - if ((b ** 2 - 1) >> 3) & 1: - result = -result - a = a >> 1 - return result - -def jacobi_witness(x, n): - """Returns False if n is an Euler pseudo-prime with base x, and - True otherwise. - """ - - j = jacobi(x, n) % n - f = fast_exponentiation(x, (n-1)/2, n) - - if j == f: return False - return True - -def randomized_primality_testing(n, k): - """Calculates whether n is composite (which is always correct) or - prime (which is incorrect with error probability 2**-k) - - Returns False if the number if composite, and True if it's - probably prime. - """ - - q = 0.5 # Property of the jacobi_witness function - - # t = int(math.ceil(k / math.log(1/q, 2))) - t = ceil(k / math.log(1/q, 2)) - for i in range(t+1): - x = randint(1, n-1) - if jacobi_witness(x, n): return False - - return True - -def is_prime(number): - """Returns True if the number is prime, and False otherwise. - """ - - """ - if not fermat_little_theorem(number) == 1: - # Not prime, according to Fermat's little theorem - return False - """ - - if randomized_primality_testing(number, 5): - # Prime, according to Jacobi - return True - - # Not prime - return False - - -def getprime(nbits): - """Returns a prime number of max. 'math.ceil(nbits/8)*8' bits. In - other words: nbits is rounded up to whole bytes. - """ - - nbytes = int(math.ceil(nbits/8.)) - - while True: - integer = read_random_int(nbits) - - # Make sure it's odd - integer |= 1 - - # Test for primeness - if is_prime(integer): break - - # Retry if not prime - - return integer - -def are_relatively_prime(a, b): - """Returns True if a and b are relatively prime, and False if they - are not. - """ - - d = gcd(a, b) - return (d == 1) - -def find_p_q(nbits): - """Returns a tuple of two different primes of nbits bits""" - - p = getprime(nbits) - while True: - q = getprime(nbits) - if not q == p: break - - return (p, q) - -def extended_euclid_gcd(a, b): - """Returns a tuple (d, i, j) such that d = gcd(a, b) = ia + jb - """ - - if b == 0: - return (a, 1, 0) - - q = abs(a % b) - r = long(a / b) - (d, k, l) = extended_euclid_gcd(b, q) - - return (d, l, k - l*r) - -# Main function: calculate encryption and decryption keys -def calculate_keys(p, q, nbits): - """Calculates an encryption and a decryption key for p and q, and - returns them as a tuple (e, d)""" - - n = p * q - phi_n = (p-1) * (q-1) - - while True: - # Make sure e has enough bits so we ensure "wrapping" through - # modulo n - e = getprime(max(8, nbits/2)) - if are_relatively_prime(e, n) and are_relatively_prime(e, phi_n): break - - (d, i, j) = extended_euclid_gcd(e, phi_n) - - if not d == 1: - raise Exception("e (%d) and phi_n (%d) are not relatively prime" % (e, phi_n)) - - if not (e * i) % phi_n == 1: - raise Exception("e (%d) and i (%d) are not mult. inv. modulo phi_n (%d)" % (e, i, phi_n)) - - return (e, i) - - -def gen_keys(nbits): - """Generate RSA keys of nbits bits. Returns (p, q, e, d). - - Note: this can take a long time, depending on the key size. - """ - - while True: - (p, q) = find_p_q(nbits) - (e, d) = calculate_keys(p, q, nbits) - - # For some reason, d is sometimes negative. We don't know how - # to fix it (yet), so we keep trying until everything is shiny - if d > 0: break - - return (p, q, e, d) - -def gen_pubpriv_keys(nbits): - """Generates public and private keys, and returns them as (pub, - priv). - - The public key consists of a dict {e: ..., , n: ....). The private - key consists of a dict {d: ...., p: ...., q: ....). - """ - - (p, q, e, d) = gen_keys(nbits) - - return ( {'e': e, 'n': p*q}, {'d': d, 'p': p, 'q': q} ) - -def encrypt_int(message, ekey, n): - """Encrypts a message using encryption key 'ekey', working modulo - n""" - - if type(message) is types.IntType: - return encrypt_int(long(message), ekey, n) - - if not type(message) is types.LongType: - raise TypeError("You must pass a long or an int") - - if message > 0 and \ - math.floor(math.log(message, 2)) > math.floor(math.log(n, 2)): - raise OverflowError("The message is too long") - - return fast_exponentiation(message, ekey, n) - -def decrypt_int(cyphertext, dkey, n): - """Decrypts a cypher text using the decryption key 'dkey', working - modulo n""" - - return encrypt_int(cyphertext, dkey, n) - -def sign_int(message, dkey, n): - """Signs 'message' using key 'dkey', working modulo n""" - - return decrypt_int(message, dkey, n) - -def verify_int(signed, ekey, n): - """verifies 'signed' using key 'ekey', working modulo n""" - - return encrypt_int(signed, ekey, n) - -def picklechops(chops): - """Pickles and base64encodes it's argument chops""" - - value = zlib.compress(dumps(chops)) - encoded = base64.encodestring(value) - return encoded.strip() - -def unpicklechops(string): - """base64decodes and unpickes it's argument string into chops""" - - return loads(zlib.decompress(base64.decodestring(string))) - -def chopstring(message, key, n, funcref): - """Splits 'message' into chops that are at most as long as n, - converts these into integers, and calls funcref(integer, key, n) - for each chop. - - Used by 'encrypt' and 'sign'. - """ - - msglen = len(message) - mbits = msglen * 8 - nbits = int(math.floor(math.log(n, 2))) - nbytes = nbits / 8 - blocks = msglen / nbytes - - if msglen % nbytes > 0: - blocks += 1 - - cypher = [] - - for bindex in range(blocks): - offset = bindex * nbytes - block = message[offset:offset+nbytes] - value = bytes2int(block) - cypher.append(funcref(value, key, n)) - - return picklechops(cypher) - -def gluechops(chops, key, n, funcref): - """Glues chops back together into a string. calls - funcref(integer, key, n) for each chop. - - Used by 'decrypt' and 'verify'. - """ - message = "" - - chops = unpicklechops(chops) - - for cpart in chops: - mpart = funcref(cpart, key, n) - message += int2bytes(mpart) - - return message - -def encrypt(message, key): - """Encrypts a string 'message' with the public key 'key'""" - - return chopstring(message, key['e'], key['n'], encrypt_int) - -def sign(message, key): - """Signs a string 'message' with the private key 'key'""" - - return chopstring(message, key['d'], key['p']*key['q'], decrypt_int) - -def decrypt(cypher, key): - """Decrypts a cypher with the private key 'key'""" - - return gluechops(cypher, key['d'], key['p']*key['q'], decrypt_int) - -def verify(cypher, key): - """Verifies a cypher with the public key 'key'""" - - return gluechops(cypher, key['e'], key['n'], encrypt_int) - -# Do doctest if we're not imported -if __name__ == "__main__": - import doctest - doctest.testmod() - -__all__ = ["gen_pubpriv_keys", "encrypt", "decrypt", "sign", "verify"] - diff --git a/rsa/_version200.py b/rsa/_version200.py deleted file mode 100644 index 1a16949..0000000 --- a/rsa/_version200.py +++ /dev/null @@ -1,513 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2011 Sybren A. Stüvel -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Deprecated version of the RSA module - -.. deprecated:: 3.0 - - This submodule is deprecated and will be completely removed as of version 4.0. - -""" - -__author__ = "Sybren Stuvel, Marloes de Boer, Ivo Tamboer, and Barry Mead" -__date__ = "2010-02-08" -__version__ = '2.0' - -import math -import os -import random -import sys -import types -from rsa._compat import byte - -# Display a warning that this insecure version is imported. -import warnings -warnings.warn('Insecure version of the RSA module is imported as %s' % __name__) -warnings.warn('This submodule is deprecated and will be completely removed as of version 4.0.', - DeprecationWarning) - - -def bit_size(number): - """Returns the number of bits required to hold a specific long number""" - - return int(math.ceil(math.log(number,2))) - -def gcd(p, q): - """Returns the greatest common divisor of p and q - >>> gcd(48, 180) - 12 - """ - # Iterateive Version is faster and uses much less stack space - while q != 0: - if p < q: (p,q) = (q,p) - (p,q) = (q, p % q) - return p - - -def bytes2int(bytes): - r"""Converts a list of bytes or a string to an integer - """ - - if not (type(bytes) is types.ListType or type(bytes) is types.StringType): - raise TypeError("You must pass a string or a list") - - # Convert byte stream to integer - integer = 0 - for byte in bytes: - integer *= 256 - if type(byte) is types.StringType: byte = ord(byte) - integer += byte - - return integer - -def int2bytes(number): - """ - Converts a number to a string of bytes - """ - - if not (type(number) is types.LongType or type(number) is types.IntType): - raise TypeError("You must pass a long or an int") - - string = "" - - while number > 0: - string = "%s%s" % (byte(number & 0xFF), string) - number /= 256 - - return string - -def to64(number): - """Converts a number in the range of 0 to 63 into base 64 digit - character in the range of '0'-'9', 'A'-'Z', 'a'-'z','-','_'. - """ - - if not (type(number) is types.LongType or type(number) is types.IntType): - raise TypeError("You must pass a long or an int") - - if 0 <= number <= 9: #00-09 translates to '0' - '9' - return byte(number + 48) - - if 10 <= number <= 35: - return byte(number + 55) #10-35 translates to 'A' - 'Z' - - if 36 <= number <= 61: - return byte(number + 61) #36-61 translates to 'a' - 'z' - - if number == 62: # 62 translates to '-' (minus) - return byte(45) - - if number == 63: # 63 translates to '_' (underscore) - return byte(95) - - raise ValueError('Invalid Base64 value: %i' % number) - - -def from64(number): - """Converts an ordinal character value in the range of - 0-9,A-Z,a-z,-,_ to a number in the range of 0-63. - """ - - if not (type(number) is types.LongType or type(number) is types.IntType): - raise TypeError("You must pass a long or an int") - - if 48 <= number <= 57: #ord('0') - ord('9') translates to 0-9 - return(number - 48) - - if 65 <= number <= 90: #ord('A') - ord('Z') translates to 10-35 - return(number - 55) - - if 97 <= number <= 122: #ord('a') - ord('z') translates to 36-61 - return(number - 61) - - if number == 45: #ord('-') translates to 62 - return(62) - - if number == 95: #ord('_') translates to 63 - return(63) - - raise ValueError('Invalid Base64 value: %i' % number) - - -def int2str64(number): - """Converts a number to a string of base64 encoded characters in - the range of '0'-'9','A'-'Z,'a'-'z','-','_'. - """ - - if not (type(number) is types.LongType or type(number) is types.IntType): - raise TypeError("You must pass a long or an int") - - string = "" - - while number > 0: - string = "%s%s" % (to64(number & 0x3F), string) - number /= 64 - - return string - - -def str642int(string): - """Converts a base64 encoded string into an integer. - The chars of this string in in the range '0'-'9','A'-'Z','a'-'z','-','_' - """ - - if not (type(string) is types.ListType or type(string) is types.StringType): - raise TypeError("You must pass a string or a list") - - integer = 0 - for byte in string: - integer *= 64 - if type(byte) is types.StringType: byte = ord(byte) - integer += from64(byte) - - return integer - -def read_random_int(nbits): - """Reads a random integer of approximately nbits bits rounded up - to whole bytes""" - - nbytes = int(math.ceil(nbits/8.)) - randomdata = os.urandom(nbytes) - return bytes2int(randomdata) - -def randint(minvalue, maxvalue): - """Returns a random integer x with minvalue <= x <= maxvalue""" - - # Safety - get a lot of random data even if the range is fairly - # small - min_nbits = 32 - - # The range of the random numbers we need to generate - range = (maxvalue - minvalue) + 1 - - # Which is this number of bytes - rangebytes = ((bit_size(range) + 7) / 8) - - # Convert to bits, but make sure it's always at least min_nbits*2 - rangebits = max(rangebytes * 8, min_nbits * 2) - - # Take a random number of bits between min_nbits and rangebits - nbits = random.randint(min_nbits, rangebits) - - return (read_random_int(nbits) % range) + minvalue - -def jacobi(a, b): - """Calculates the value of the Jacobi symbol (a/b) - where both a and b are positive integers, and b is odd - """ - - if a == 0: return 0 - result = 1 - while a > 1: - if a & 1: - if ((a-1)*(b-1) >> 2) & 1: - result = -result - a, b = b % a, a - else: - if (((b * b) - 1) >> 3) & 1: - result = -result - a >>= 1 - if a == 0: return 0 - return result - -def jacobi_witness(x, n): - """Returns False if n is an Euler pseudo-prime with base x, and - True otherwise. - """ - - j = jacobi(x, n) % n - f = pow(x, (n-1)/2, n) - - if j == f: return False - return True - -def randomized_primality_testing(n, k): - """Calculates whether n is composite (which is always correct) or - prime (which is incorrect with error probability 2**-k) - - Returns False if the number is composite, and True if it's - probably prime. - """ - - # 50% of Jacobi-witnesses can report compositness of non-prime numbers - - for i in range(k): - x = randint(1, n-1) - if jacobi_witness(x, n): return False - - return True - -def is_prime(number): - """Returns True if the number is prime, and False otherwise. - """ - - if randomized_primality_testing(number, 6): - # Prime, according to Jacobi - return True - - # Not prime - return False - - -def getprime(nbits): - """Returns a prime number of max. 'math.ceil(nbits/8)*8' bits. In - other words: nbits is rounded up to whole bytes. - """ - - while True: - integer = read_random_int(nbits) - - # Make sure it's odd - integer |= 1 - - # Test for primeness - if is_prime(integer): break - - # Retry if not prime - - return integer - -def are_relatively_prime(a, b): - """Returns True if a and b are relatively prime, and False if they - are not. - - >>> are_relatively_prime(2, 3) - 1 - >>> are_relatively_prime(2, 4) - 0 - """ - - d = gcd(a, b) - return (d == 1) - -def find_p_q(nbits): - """Returns a tuple of two different primes of nbits bits""" - pbits = nbits + (nbits/16) #Make sure that p and q aren't too close - qbits = nbits - (nbits/16) #or the factoring programs can factor n - p = getprime(pbits) - while True: - q = getprime(qbits) - #Make sure p and q are different. - if not q == p: break - return (p, q) - -def extended_gcd(a, b): - """Returns a tuple (r, i, j) such that r = gcd(a, b) = ia + jb - """ - # r = gcd(a,b) i = multiplicitive inverse of a mod b - # or j = multiplicitive inverse of b mod a - # Neg return values for i or j are made positive mod b or a respectively - # Iterateive Version is faster and uses much less stack space - x = 0 - y = 1 - lx = 1 - ly = 0 - oa = a #Remember original a/b to remove - ob = b #negative values from return results - while b != 0: - q = long(a/b) - (a, b) = (b, a % b) - (x, lx) = ((lx - (q * x)),x) - (y, ly) = ((ly - (q * y)),y) - if (lx < 0): lx += ob #If neg wrap modulo orignal b - if (ly < 0): ly += oa #If neg wrap modulo orignal a - return (a, lx, ly) #Return only positive values - -# Main function: calculate encryption and decryption keys -def calculate_keys(p, q, nbits): - """Calculates an encryption and a decryption key for p and q, and - returns them as a tuple (e, d)""" - - n = p * q - phi_n = (p-1) * (q-1) - - while True: - # Make sure e has enough bits so we ensure "wrapping" through - # modulo n - e = max(65537,getprime(nbits/4)) - if are_relatively_prime(e, n) and are_relatively_prime(e, phi_n): break - - (d, i, j) = extended_gcd(e, phi_n) - - if not d == 1: - raise Exception("e (%d) and phi_n (%d) are not relatively prime" % (e, phi_n)) - if (i < 0): - raise Exception("New extended_gcd shouldn't return negative values") - if not (e * i) % phi_n == 1: - raise Exception("e (%d) and i (%d) are not mult. inv. modulo phi_n (%d)" % (e, i, phi_n)) - - return (e, i) - - -def gen_keys(nbits): - """Generate RSA keys of nbits bits. Returns (p, q, e, d). - - Note: this can take a long time, depending on the key size. - """ - - (p, q) = find_p_q(nbits) - (e, d) = calculate_keys(p, q, nbits) - - return (p, q, e, d) - -def newkeys(nbits): - """Generates public and private keys, and returns them as (pub, - priv). - - The public key consists of a dict {e: ..., , n: ....). The private - key consists of a dict {d: ...., p: ...., q: ....). - """ - nbits = max(9,nbits) # Don't let nbits go below 9 bits - (p, q, e, d) = gen_keys(nbits) - - return ( {'e': e, 'n': p*q}, {'d': d, 'p': p, 'q': q} ) - -def encrypt_int(message, ekey, n): - """Encrypts a message using encryption key 'ekey', working modulo n""" - - if type(message) is types.IntType: - message = long(message) - - if not type(message) is types.LongType: - raise TypeError("You must pass a long or int") - - if message < 0 or message > n: - raise OverflowError("The message is too long") - - #Note: Bit exponents start at zero (bit counts start at 1) this is correct - safebit = bit_size(n) - 2 #compute safe bit (MSB - 1) - message += (1 << safebit) #add safebit to ensure folding - - return pow(message, ekey, n) - -def decrypt_int(cyphertext, dkey, n): - """Decrypts a cypher text using the decryption key 'dkey', working - modulo n""" - - message = pow(cyphertext, dkey, n) - - safebit = bit_size(n) - 2 #compute safe bit (MSB - 1) - message -= (1 << safebit) #remove safebit before decode - - return message - -def encode64chops(chops): - """base64encodes chops and combines them into a ',' delimited string""" - - chips = [] #chips are character chops - - for value in chops: - chips.append(int2str64(value)) - - #delimit chops with comma - encoded = ','.join(chips) - - return encoded - -def decode64chops(string): - """base64decodes and makes a ',' delimited string into chops""" - - chips = string.split(',') #split chops at commas - - chops = [] - - for string in chips: #make char chops (chips) into chops - chops.append(str642int(string)) - - return chops - -def chopstring(message, key, n, funcref): - """Chops the 'message' into integers that fit into n, - leaving room for a safebit to be added to ensure that all - messages fold during exponentiation. The MSB of the number n - is not independant modulo n (setting it could cause overflow), so - use the next lower bit for the safebit. Therefore reserve 2-bits - in the number n for non-data bits. Calls specified encryption - function for each chop. - - Used by 'encrypt' and 'sign'. - """ - - msglen = len(message) - mbits = msglen * 8 - #Set aside 2-bits so setting of safebit won't overflow modulo n. - nbits = bit_size(n) - 2 # leave room for safebit - nbytes = nbits / 8 - blocks = msglen / nbytes - - if msglen % nbytes > 0: - blocks += 1 - - cypher = [] - - for bindex in range(blocks): - offset = bindex * nbytes - block = message[offset:offset+nbytes] - value = bytes2int(block) - cypher.append(funcref(value, key, n)) - - return encode64chops(cypher) #Encode encrypted ints to base64 strings - -def gluechops(string, key, n, funcref): - """Glues chops back together into a string. calls - funcref(integer, key, n) for each chop. - - Used by 'decrypt' and 'verify'. - """ - message = "" - - chops = decode64chops(string) #Decode base64 strings into integer chops - - for cpart in chops: - mpart = funcref(cpart, key, n) #Decrypt each chop - message += int2bytes(mpart) #Combine decrypted strings into a msg - - return message - -def encrypt(message, key): - """Encrypts a string 'message' with the public key 'key'""" - if 'n' not in key: - raise Exception("You must use the public key with encrypt") - - return chopstring(message, key['e'], key['n'], encrypt_int) - -def sign(message, key): - """Signs a string 'message' with the private key 'key'""" - if 'p' not in key: - raise Exception("You must use the private key with sign") - - return chopstring(message, key['d'], key['p']*key['q'], encrypt_int) - -def decrypt(cypher, key): - """Decrypts a string 'cypher' with the private key 'key'""" - if 'p' not in key: - raise Exception("You must use the private key with decrypt") - - return gluechops(cypher, key['d'], key['p']*key['q'], decrypt_int) - -def verify(cypher, key): - """Verifies a string 'cypher' with the public key 'key'""" - if 'n' not in key: - raise Exception("You must use the public key with verify") - - return gluechops(cypher, key['e'], key['n'], decrypt_int) - -# Do doctest if we're not imported -if __name__ == "__main__": - import doctest - doctest.testmod() - -__all__ = ["newkeys", "encrypt", "decrypt", "sign", "verify"] - diff --git a/rsa/asn1.py b/rsa/asn1.py index b724b8f..32b1eb4 100644 --- a/rsa/asn1.py +++ b/rsa/asn1.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/rsa/bigfile.py b/rsa/bigfile.py deleted file mode 100644 index 3a09716..0000000 --- a/rsa/bigfile.py +++ /dev/null @@ -1,135 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2011 Sybren A. Stüvel -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Large file support - -.. deprecated:: 3.4 - - The VARBLOCK format is NOT recommended for general use, has been deprecated since - Python-RSA 3.4, and will be removed in a future release. It's vulnerable to a - number of attacks: - - 1. decrypt/encrypt_bigfile() does not implement `Authenticated encryption`_ nor - uses MACs to verify messages before decrypting public key encrypted messages. - - 2. decrypt/encrypt_bigfile() does not use hybrid encryption (it uses plain RSA) - and has no method for chaining, so block reordering is possible. - - See `issue #19 on Github`_ for more information. - -.. _Authenticated encryption: https://en.wikipedia.org/wiki/Authenticated_encryption -.. _issue #19 on Github: https://github.com/sybrenstuvel/python-rsa/issues/13 - - -This module contains functions to: - - - break a file into smaller blocks, and encrypt them, and store the - encrypted blocks in another file. - - - take such an encrypted files, decrypt its blocks, and reconstruct the - original file. - -The encrypted file format is as follows, where || denotes byte concatenation: - - FILE := VERSION || BLOCK || BLOCK ... - - BLOCK := LENGTH || DATA - - LENGTH := varint-encoded length of the subsequent data. Varint comes from - Google Protobuf, and encodes an integer into a variable number of bytes. - Each byte uses the 7 lowest bits to encode the value. The highest bit set - to 1 indicates the next byte is also part of the varint. The last byte will - have this bit set to 0. - -This file format is called the VARBLOCK format, in line with the varint format -used to denote the block sizes. - -""" - -import warnings - -from rsa import key, common, pkcs1, varblock -from rsa._compat import byte - - -def encrypt_bigfile(infile, outfile, pub_key): - """Encrypts a file, writing it to 'outfile' in VARBLOCK format. - - .. deprecated:: 3.4 - This function was deprecated in Python-RSA version 3.4 due to security issues - in the VARBLOCK format. See the documentation_ for more information. - - .. _documentation: https://stuvel.eu/python-rsa-doc/usage.html#working-with-big-files - - :param infile: file-like object to read the cleartext from - :param outfile: file-like object to write the crypto in VARBLOCK format to - :param pub_key: :py:class:`rsa.PublicKey` to encrypt with - - """ - - warnings.warn("The 'rsa.bigfile.encrypt_bigfile' function was deprecated in Python-RSA version " - "3.4 due to security issues in the VARBLOCK format. See " - "https://stuvel.eu/python-rsa-doc/usage.html#working-with-big-files " - "for more information.", - DeprecationWarning, stacklevel=2) - - if not isinstance(pub_key, key.PublicKey): - raise TypeError('Public key required, but got %r' % pub_key) - - key_bytes = common.bit_size(pub_key.n) // 8 - blocksize = key_bytes - 11 # keep space for PKCS#1 padding - - # Write the version number to the VARBLOCK file - outfile.write(byte(varblock.VARBLOCK_VERSION)) - - # Encrypt and write each block - for block in varblock.yield_fixedblocks(infile, blocksize): - crypto = pkcs1.encrypt(block, pub_key) - - varblock.write_varint(outfile, len(crypto)) - outfile.write(crypto) - - -def decrypt_bigfile(infile, outfile, priv_key): - """Decrypts an encrypted VARBLOCK file, writing it to 'outfile' - - .. deprecated:: 3.4 - This function was deprecated in Python-RSA version 3.4 due to security issues - in the VARBLOCK format. See the documentation_ for more information. - - .. _documentation: https://stuvel.eu/python-rsa-doc/usage.html#working-with-big-files - - :param infile: file-like object to read the crypto in VARBLOCK format from - :param outfile: file-like object to write the cleartext to - :param priv_key: :py:class:`rsa.PrivateKey` to decrypt with - - """ - - warnings.warn("The 'rsa.bigfile.decrypt_bigfile' function was deprecated in Python-RSA version " - "3.4 due to security issues in the VARBLOCK format. See " - "https://stuvel.eu/python-rsa-doc/usage.html#working-with-big-files " - "for more information.", - DeprecationWarning, stacklevel=2) - - if not isinstance(priv_key, key.PrivateKey): - raise TypeError('Private key required, but got %r' % priv_key) - - for block in varblock.yield_varblocks(infile): - cleartext = pkcs1.decrypt(block, priv_key) - outfile.write(cleartext) - - -__all__ = ['encrypt_bigfile', 'decrypt_bigfile'] diff --git a/rsa/cli.py b/rsa/cli.py index 3a21878..3166150 100644 --- a/rsa/cli.py +++ b/rsa/cli.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,25 +17,25 @@ These scripts are called by the executables defined in setup.py. """ -from __future__ import with_statement, print_function - import abc import sys -from optparse import OptionParser +import typing +import optparse import rsa -import rsa.bigfile +import rsa.key import rsa.pkcs1 HASH_METHODS = sorted(rsa.pkcs1.HASH_METHODS.keys()) +Indexable = typing.Union[typing.Tuple, typing.List[str]] -def keygen(): +def keygen() -> None: """Key generator.""" # Parse the CLI options - parser = OptionParser(usage='usage: %prog [options] keysize', - description='Generates a new RSA keypair of "keysize" bits.') + parser = optparse.OptionParser(usage='usage: %prog [options] keysize', + description='Generates a new RSA keypair of "keysize" bits.') parser.add_option('--pubout', type='string', help='Output filename for the public key. The public key is ' @@ -84,17 +82,15 @@ def keygen(): outfile.write(data) else: print('Writing private key to stdout', file=sys.stderr) - sys.stdout.write(data) + sys.stdout.buffer.write(data) -class CryptoOperation(object): +class CryptoOperation(metaclass=abc.ABCMeta): """CLI callable that operates with input, output, and a key.""" - __metaclass__ = abc.ABCMeta - keyname = 'public' # or 'private' usage = 'usage: %%prog [options] %(keyname)s_key' - description = None + description = '' operation = 'decrypt' operation_past = 'decrypted' operation_progressive = 'decrypting' @@ -105,15 +101,16 @@ class CryptoOperation(object): expected_cli_args = 1 has_output = True - key_class = rsa.PublicKey + key_class = rsa.PublicKey # type: typing.Type[rsa.key.AbstractKey] - def __init__(self): + def __init__(self) -> None: self.usage = self.usage % self.__class__.__dict__ self.input_help = self.input_help % self.__class__.__dict__ self.output_help = self.output_help % self.__class__.__dict__ @abc.abstractmethod - def perform_operation(self, indata, key, cli_args=None): + def perform_operation(self, indata: bytes, key: rsa.key.AbstractKey, + cli_args: Indexable) -> typing.Any: """Performs the program's operation. Implement in a subclass. @@ -121,7 +118,7 @@ def perform_operation(self, indata, key, cli_args=None): :returns: the data to write to the output. """ - def __call__(self): + def __call__(self) -> None: """Runs the program.""" (cli, cli_args) = self.parse_cli() @@ -136,13 +133,13 @@ def __call__(self): if self.has_output: self.write_outfile(outdata, cli.output) - def parse_cli(self): + def parse_cli(self) -> typing.Tuple[optparse.Values, typing.List[str]]: """Parse the CLI options :returns: (cli_opts, cli_args) """ - parser = OptionParser(usage=self.usage, description=self.description) + parser = optparse.OptionParser(usage=self.usage, description=self.description) parser.add_option('-i', '--input', type='string', help=self.input_help) @@ -161,7 +158,7 @@ def parse_cli(self): return cli, cli_args - def read_key(self, filename, keyform): + def read_key(self, filename: str, keyform: str) -> rsa.key.AbstractKey: """Reads a public or private key.""" print('Reading %s key from %s' % (self.keyname, filename), file=sys.stderr) @@ -170,7 +167,7 @@ def read_key(self, filename, keyform): return self.key_class.load_pkcs1(keydata, keyform) - def read_infile(self, inname): + def read_infile(self, inname: str) -> bytes: """Read the input file""" if inname: @@ -179,9 +176,9 @@ def read_infile(self, inname): return infile.read() print('Reading input from stdin', file=sys.stderr) - return sys.stdin.read() + return sys.stdin.buffer.read() - def write_outfile(self, outdata, outname): + def write_outfile(self, outdata: bytes, outname: str) -> None: """Write the output file""" if outname: @@ -190,7 +187,7 @@ def write_outfile(self, outdata, outname): outfile.write(outdata) else: print('Writing output to stdout', file=sys.stderr) - sys.stdout.write(outdata) + sys.stdout.buffer.write(outdata) class EncryptOperation(CryptoOperation): @@ -198,15 +195,15 @@ class EncryptOperation(CryptoOperation): keyname = 'public' description = ('Encrypts a file. The file must be shorter than the key ' - 'length in order to be encrypted. For larger files, use the ' - 'pyrsa-encrypt-bigfile command.') + 'length in order to be encrypted.') operation = 'encrypt' operation_past = 'encrypted' operation_progressive = 'encrypting' - def perform_operation(self, indata, pub_key, cli_args=None): + def perform_operation(self, indata: bytes, pub_key: rsa.key.AbstractKey, + cli_args: Indexable = ()) -> bytes: """Encrypts files.""" - + assert isinstance(pub_key, rsa.key.PublicKey) return rsa.encrypt(indata, pub_key) @@ -215,16 +212,16 @@ class DecryptOperation(CryptoOperation): keyname = 'private' description = ('Decrypts a file. The original file must be shorter than ' - 'the key length in order to have been encrypted. For larger ' - 'files, use the pyrsa-decrypt-bigfile command.') + 'the key length in order to have been encrypted.') operation = 'decrypt' operation_past = 'decrypted' operation_progressive = 'decrypting' key_class = rsa.PrivateKey - def perform_operation(self, indata, priv_key, cli_args=None): + def perform_operation(self, indata: bytes, priv_key: rsa.key.AbstractKey, + cli_args: Indexable = ()) -> bytes: """Decrypts files.""" - + assert isinstance(priv_key, rsa.key.PrivateKey) return rsa.decrypt(indata, priv_key) @@ -244,8 +241,10 @@ class SignOperation(CryptoOperation): output_help = ('Name of the file to write the signature to. Written ' 'to stdout if this option is not present.') - def perform_operation(self, indata, priv_key, cli_args): + def perform_operation(self, indata: bytes, priv_key: rsa.key.AbstractKey, + cli_args: Indexable) -> bytes: """Signs files.""" + assert isinstance(priv_key, rsa.key.PrivateKey) hash_method = cli_args[1] if hash_method not in HASH_METHODS: @@ -269,8 +268,10 @@ class VerifyOperation(CryptoOperation): expected_cli_args = 2 has_output = False - def perform_operation(self, indata, pub_key, cli_args): + def perform_operation(self, indata: bytes, pub_key: rsa.key.AbstractKey, + cli_args: Indexable) -> None: """Verifies files.""" + assert isinstance(pub_key, rsa.key.PublicKey) signature_file = cli_args[1] @@ -285,99 +286,7 @@ def perform_operation(self, indata, pub_key, cli_args): print('Verification OK', file=sys.stderr) -class BigfileOperation(CryptoOperation): - """CryptoOperation that doesn't read the entire file into memory.""" - - def __init__(self): - CryptoOperation.__init__(self) - - self.file_objects = [] - - def __del__(self): - """Closes any open file handles.""" - - for fobj in self.file_objects: - fobj.close() - - def __call__(self): - """Runs the program.""" - - (cli, cli_args) = self.parse_cli() - - key = self.read_key(cli_args[0], cli.keyform) - - # Get the file handles - infile = self.get_infile(cli.input) - outfile = self.get_outfile(cli.output) - - # Call the operation - print(self.operation_progressive.title(), file=sys.stderr) - self.perform_operation(infile, outfile, key, cli_args) - - def get_infile(self, inname): - """Returns the input file object""" - - if inname: - print('Reading input from %s' % inname, file=sys.stderr) - fobj = open(inname, 'rb') - self.file_objects.append(fobj) - else: - print('Reading input from stdin', file=sys.stderr) - fobj = sys.stdin - - return fobj - - def get_outfile(self, outname): - """Returns the output file object""" - - if outname: - print('Will write output to %s' % outname, file=sys.stderr) - fobj = open(outname, 'wb') - self.file_objects.append(fobj) - else: - print('Will write output to stdout', file=sys.stderr) - fobj = sys.stdout - - return fobj - - -class EncryptBigfileOperation(BigfileOperation): - """Encrypts a file to VARBLOCK format.""" - - keyname = 'public' - description = ('Encrypts a file to an encrypted VARBLOCK file. The file ' - 'can be larger than the key length, but the output file is only ' - 'compatible with Python-RSA.') - operation = 'encrypt' - operation_past = 'encrypted' - operation_progressive = 'encrypting' - - def perform_operation(self, infile, outfile, pub_key, cli_args=None): - """Encrypts files to VARBLOCK.""" - - return rsa.bigfile.encrypt_bigfile(infile, outfile, pub_key) - - -class DecryptBigfileOperation(BigfileOperation): - """Decrypts a file in VARBLOCK format.""" - - keyname = 'private' - description = ('Decrypts an encrypted VARBLOCK file that was encrypted ' - 'with pyrsa-encrypt-bigfile') - operation = 'decrypt' - operation_past = 'decrypted' - operation_progressive = 'decrypting' - key_class = rsa.PrivateKey - - def perform_operation(self, infile, outfile, priv_key, cli_args=None): - """Decrypts a VARBLOCK file.""" - - return rsa.bigfile.decrypt_bigfile(infile, outfile, priv_key) - - encrypt = EncryptOperation() decrypt = DecryptOperation() sign = SignOperation() verify = VerifyOperation() -encrypt_bigfile = EncryptBigfileOperation() -decrypt_bigfile = DecryptBigfileOperation() diff --git a/rsa/common.py b/rsa/common.py index e074334..e7df21d 100644 --- a/rsa/common.py +++ b/rsa/common.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,15 +14,22 @@ """Common functionality shared by several modules.""" +import typing + + +class NotRelativePrimeError(ValueError): + def __init__(self, a: int, b: int, d: int, msg: str = '') -> None: + super().__init__(msg or "%d and %d are not relatively prime, divider=%i" % (a, b, d)) + self.a = a + self.b = b + self.d = d + -def bit_size(num): +def bit_size(num: int) -> int: """ Number of bits needed to represent a integer excluding any prefix 0 bits. - As per definition from https://wiki.python.org/moin/BitManipulation and - to match the behavior of the Python 3 API. - Usage:: >>> bit_size(1023) @@ -41,44 +46,14 @@ def bit_size(num): :returns: Returns the number of bits in the integer. """ - if num == 0: - return 0 - if num < 0: - num = -num - - # Make sure this is an int and not a float. - num & 1 - hex_num = "%x" % num - return ((len(hex_num) - 1) * 4) + { - '0': 0, '1': 1, '2': 2, '3': 2, - '4': 3, '5': 3, '6': 3, '7': 3, - '8': 4, '9': 4, 'a': 4, 'b': 4, - 'c': 4, 'd': 4, 'e': 4, 'f': 4, - }[hex_num[0]] + try: + return num.bit_length() + except AttributeError: + raise TypeError('bit_size(num) only supports integers, not %r' % type(num)) -def _bit_size(number): - """ - Returns the number of bits required to hold a specific long number. - """ - if number < 0: - raise ValueError('Only nonnegative numbers possible: %s' % number) - - if number == 0: - return 0 - - # This works, even with very large numbers. When using math.log(number, 2), - # you'll get rounding errors and it'll fail. - bits = 0 - while number: - bits += 1 - number >>= 1 - - return bits - - -def byte_size(number): +def byte_size(number: int) -> int: """ Returns the number of bytes required to hold a specific long number. @@ -98,14 +73,36 @@ def byte_size(number): :returns: The number of bytes required to hold a specific long number. """ - quanta, mod = divmod(bit_size(number), 8) - if mod or number == 0: + if number == 0: + return 1 + return ceil_div(bit_size(number), 8) + + +def ceil_div(num: int, div: int) -> int: + """ + Returns the ceiling function of a division between `num` and `div`. + + Usage:: + + >>> ceil_div(100, 7) + 15 + >>> ceil_div(100, 10) + 10 + >>> ceil_div(1, 4) + 1 + + :param num: Division's numerator, a number + :param div: Division's divisor, a number + + :return: Rounded up result of the division between the parameters. + """ + quanta, mod = divmod(num, div) + if mod: quanta += 1 return quanta - # return int(math.ceil(bit_size(number) / 8.0)) -def extended_gcd(a, b): +def extended_gcd(a: int, b: int) -> typing.Tuple[int, int, int]: """Returns a tuple (r, i, j) such that r = gcd(a, b) = ia + jb """ # r = gcd(a,b) i = multiplicitive inverse of a mod b @@ -130,8 +127,8 @@ def extended_gcd(a, b): return a, lx, ly # Return only positive values -def inverse(x, n): - """Returns x^-1 (mod n) +def inverse(x: int, n: int) -> int: + """Returns the inverse of x % n under multiplication, a.k.a x^-1 (mod n) >>> inverse(7, 4) 3 @@ -142,12 +139,12 @@ def inverse(x, n): (divider, inv, _) = extended_gcd(x, n) if divider != 1: - raise ValueError("x (%d) and n (%d) are not relatively prime" % (x, n)) + raise NotRelativePrimeError(x, n, divider) return inv -def crt(a_values, modulo_values): +def crt(a_values: typing.Iterable[int], modulo_values: typing.Iterable[int]) -> int: """Chinese Remainder Theorem. Calculates x such that x = a[i] (mod m[i]) for each i. diff --git a/rsa/core.py b/rsa/core.py index b3114d9..23032e3 100644 --- a/rsa/core.py +++ b/rsa/core.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,17 +18,15 @@ mathematically on integers. """ -from rsa._compat import is_integer - -def assert_int(var, name): - if is_integer(var): +def assert_int(var: int, name: str) -> None: + if isinstance(var, int): return raise TypeError('%s should be an integer, not %s' % (name, var.__class__)) -def encrypt_int(message, ekey, n): +def encrypt_int(message: int, ekey: int, n: int) -> int: """Encrypts a message using encryption key 'ekey', working modulo n""" assert_int(message, 'message') @@ -46,7 +42,7 @@ def encrypt_int(message, ekey, n): return pow(message, ekey, n) -def decrypt_int(cyphertext, dkey, n): +def decrypt_int(cyphertext: int, dkey: int, n: int) -> int: """Decrypts a cypher text using the decryption key 'dkey', working modulo n""" assert_int(cyphertext, 'cyphertext') diff --git a/rsa/key.py b/rsa/key.py index 64600a2..b1e2030 100644 --- a/rsa/key.py +++ b/rsa/key.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,7 +32,8 @@ """ import logging -from rsa._compat import b +import typing +import warnings import rsa.prime import rsa.pem @@ -42,28 +41,70 @@ import rsa.randnum import rsa.core + log = logging.getLogger(__name__) DEFAULT_EXPONENT = 65537 -class AbstractKey(object): +class AbstractKey: """Abstract superclass for private and public keys.""" __slots__ = ('n', 'e') - def __init__(self, n, e): + def __init__(self, n: int, e: int) -> None: self.n = n self.e = e @classmethod - def load_pkcs1(cls, keyfile, format='PEM'): + def _load_pkcs1_pem(cls, keyfile: bytes) -> 'AbstractKey': + """Loads a key in PKCS#1 PEM format, implement in a subclass. + + :param keyfile: contents of a PEM-encoded file that contains + the public key. + :type keyfile: bytes + + :return: the loaded key + :rtype: AbstractKey + """ + + @classmethod + def _load_pkcs1_der(cls, keyfile: bytes) -> 'AbstractKey': + """Loads a key in PKCS#1 PEM format, implement in a subclass. + + :param keyfile: contents of a DER-encoded file that contains + the public key. + :type keyfile: bytes + + :return: the loaded key + :rtype: AbstractKey + """ + + def _save_pkcs1_pem(self) -> bytes: + """Saves the key in PKCS#1 PEM format, implement in a subclass. + + :returns: the PEM-encoded key. + :rtype: bytes + """ + + def _save_pkcs1_der(self) -> bytes: + """Saves the key in PKCS#1 DER format, implement in a subclass. + + :returns: the DER-encoded key. + :rtype: bytes + """ + + @classmethod + def load_pkcs1(cls, keyfile: bytes, format: str = 'PEM') -> 'AbstractKey': """Loads a key in PKCS#1 DER or PEM format. :param keyfile: contents of a DER- or PEM-encoded file that contains - the public key. + the key. + :type keyfile: bytes :param format: the format of the file to load; 'PEM' or 'DER' + :type format: str - :return: a PublicKey object + :return: the loaded key + :rtype: AbstractKey """ methods = { @@ -75,7 +116,8 @@ def load_pkcs1(cls, keyfile, format='PEM'): return method(keyfile) @staticmethod - def _assert_format_exists(file_format, methods): + def _assert_format_exists(file_format: str, methods: typing.Mapping[str, typing.Callable]) \ + -> typing.Callable: """Checks whether the given file format exists in 'methods'. """ @@ -86,11 +128,13 @@ def _assert_format_exists(file_format, methods): raise ValueError('Unsupported format: %r, try one of %s' % (file_format, formats)) - def save_pkcs1(self, format='PEM'): - """Saves the public key in PKCS#1 DER or PEM format. + def save_pkcs1(self, format: str = 'PEM') -> bytes: + """Saves the key in PKCS#1 DER or PEM format. :param format: the format to save; 'PEM' or 'DER' - :returns: the DER- or PEM-encoded public key. + :type format: str + :returns: the DER- or PEM-encoded key. + :rtype: bytes """ methods = { @@ -101,7 +145,7 @@ def save_pkcs1(self, format='PEM'): method = self._assert_format_exists(format, methods) return method() - def blind(self, message, r): + def blind(self, message: int, r: int) -> int: """Performs blinding on the message using random number 'r'. :param message: the message, as integer, to blind. @@ -118,7 +162,7 @@ def blind(self, message, r): return (message * pow(r, self.e, self.n)) % self.n - def unblind(self, blinded, r): + def unblind(self, blinded: int, r: int) -> int: """Performs blinding on the message using random number 'r'. :param blinded: the blinded message, as integer, to unblind. @@ -139,7 +183,7 @@ class PublicKey(AbstractKey): This key is also known as the 'encryption key'. It contains the 'n' and 'e' values. - Supports attributes as well as dictionary-like access. Attribute accesss is + Supports attributes as well as dictionary-like access. Attribute access is faster, though. >>> PublicKey(5, 3) @@ -159,21 +203,21 @@ class PublicKey(AbstractKey): __slots__ = ('n', 'e') - def __getitem__(self, key): + def __getitem__(self, key: str) -> int: return getattr(self, key) - def __repr__(self): + def __repr__(self) -> str: return 'PublicKey(%i, %i)' % (self.n, self.e) - def __getstate__(self): + def __getstate__(self) -> typing.Tuple[int, int]: """Returns the key as tuple for pickling.""" return self.n, self.e - def __setstate__(self, state): + def __setstate__(self, state: typing.Tuple[int, int]) -> None: """Sets the key from tuple.""" self.n, self.e = state - def __eq__(self, other): + def __eq__(self, other: typing.Any) -> bool: if other is None: return False @@ -182,11 +226,14 @@ def __eq__(self, other): return self.n == other.n and self.e == other.e - def __ne__(self, other): + def __ne__(self, other: typing.Any) -> bool: return not (self == other) + def __hash__(self) -> int: + return hash((self.n, self.e)) + @classmethod - def _load_pkcs1_der(cls, keyfile): + def _load_pkcs1_der(cls, keyfile: bytes) -> 'PublicKey': """Loads a key in PKCS#1 DER format. :param keyfile: contents of a DER-encoded file that contains the public @@ -212,10 +259,11 @@ def _load_pkcs1_der(cls, keyfile): (priv, _) = decoder.decode(keyfile, asn1Spec=AsnPubKey()) return cls(n=int(priv['modulus']), e=int(priv['publicExponent'])) - def _save_pkcs1_der(self): + def _save_pkcs1_der(self) -> bytes: """Saves the public key in PKCS#1 DER format. - @returns: the DER-encoded public key. + :returns: the DER-encoded public key. + :rtype: bytes """ from pyasn1.codec.der import encoder @@ -229,7 +277,7 @@ def _save_pkcs1_der(self): return encoder.encode(asn_key) @classmethod - def _load_pkcs1_pem(cls, keyfile): + def _load_pkcs1_pem(cls, keyfile: bytes) -> 'PublicKey': """Loads a PKCS#1 PEM-encoded public key file. The contents of the file before the "-----BEGIN RSA PUBLIC KEY-----" and @@ -243,17 +291,18 @@ def _load_pkcs1_pem(cls, keyfile): der = rsa.pem.load_pem(keyfile, 'RSA PUBLIC KEY') return cls._load_pkcs1_der(der) - def _save_pkcs1_pem(self): + def _save_pkcs1_pem(self) -> bytes: """Saves a PKCS#1 PEM-encoded public key file. :return: contents of a PEM-encoded file that contains the public key. + :rtype: bytes """ der = self._save_pkcs1_der() return rsa.pem.save_pem(der, 'RSA PUBLIC KEY') @classmethod - def load_pkcs1_openssl_pem(cls, keyfile): + def load_pkcs1_openssl_pem(cls, keyfile: bytes) -> 'PublicKey': """Loads a PKCS#1.5 PEM-encoded public key file from OpenSSL. These files can be recognised in that they start with BEGIN PUBLIC KEY @@ -264,6 +313,7 @@ def load_pkcs1_openssl_pem(cls, keyfile): :param keyfile: contents of a PEM-encoded file that contains the public key, from OpenSSL. + :type keyfile: bytes :return: a PublicKey object """ @@ -271,13 +321,12 @@ def load_pkcs1_openssl_pem(cls, keyfile): return cls.load_pkcs1_openssl_der(der) @classmethod - def load_pkcs1_openssl_der(cls, keyfile): + def load_pkcs1_openssl_der(cls, keyfile: bytes) -> 'PublicKey': """Loads a PKCS#1 DER-encoded public key file from OpenSSL. :param keyfile: contents of a DER-encoded file that contains the public key, from OpenSSL. :return: a PublicKey object - """ from rsa.asn1 import OpenSSLPubKey @@ -298,73 +347,52 @@ class PrivateKey(AbstractKey): This key is also known as the 'decryption key'. It contains the 'n', 'e', 'd', 'p', 'q' and other values. - Supports attributes as well as dictionary-like access. Attribute accesss is + Supports attributes as well as dictionary-like access. Attribute access is faster, though. >>> PrivateKey(3247, 65537, 833, 191, 17) PrivateKey(3247, 65537, 833, 191, 17) - exp1, exp2 and coef can be given, but if None or omitted they will be calculated: + exp1, exp2 and coef will be calculated: - >>> pk = PrivateKey(3727264081, 65537, 3349121513, 65063, 57287, exp2=4) + >>> pk = PrivateKey(3727264081, 65537, 3349121513, 65063, 57287) >>> pk.exp1 55063 - >>> pk.exp2 # this is of course not a correct value, but it is the one we passed. - 4 - >>> pk.coef - 50797 - - If you give exp1, exp2 or coef, they will be used as-is: - - >>> pk = PrivateKey(1, 2, 3, 4, 5, 6, 7, 8) - >>> pk.exp1 - 6 >>> pk.exp2 - 7 + 10095 >>> pk.coef - 8 + 50797 """ __slots__ = ('n', 'e', 'd', 'p', 'q', 'exp1', 'exp2', 'coef') - def __init__(self, n, e, d, p, q, exp1=None, exp2=None, coef=None): + def __init__(self, n: int, e: int, d: int, p: int, q: int) -> None: AbstractKey.__init__(self, n, e) self.d = d self.p = p self.q = q - # Calculate the other values if they aren't supplied - if exp1 is None: - self.exp1 = int(d % (p - 1)) - else: - self.exp1 = exp1 - - if exp2 is None: - self.exp2 = int(d % (q - 1)) - else: - self.exp2 = exp2 + # Calculate exponents and coefficient. + self.exp1 = int(d % (p - 1)) + self.exp2 = int(d % (q - 1)) + self.coef = rsa.common.inverse(q, p) - if coef is None: - self.coef = rsa.common.inverse(q, p) - else: - self.coef = coef - - def __getitem__(self, key): + def __getitem__(self, key: str) -> int: return getattr(self, key) - def __repr__(self): - return 'PrivateKey(%(n)i, %(e)i, %(d)i, %(p)i, %(q)i)' % self + def __repr__(self) -> str: + return 'PrivateKey(%i, %i, %i, %i, %i)' % (self.n, self.e, self.d, self.p, self.q) - def __getstate__(self): + def __getstate__(self) -> typing.Tuple[int, int, int, int, int, int, int, int]: """Returns the key as tuple for pickling.""" return self.n, self.e, self.d, self.p, self.q, self.exp1, self.exp2, self.coef - def __setstate__(self, state): + def __setstate__(self, state: typing.Tuple[int, int, int, int, int, int, int, int]) -> None: """Sets the key from tuple.""" self.n, self.e, self.d, self.p, self.q, self.exp1, self.exp2, self.coef = state - def __eq__(self, other): + def __eq__(self, other: typing.Any) -> bool: if other is None: return False @@ -380,10 +408,20 @@ def __eq__(self, other): self.exp2 == other.exp2 and self.coef == other.coef) - def __ne__(self, other): + def __ne__(self, other: typing.Any) -> bool: return not (self == other) - def blinded_decrypt(self, encrypted): + def __hash__(self) -> int: + return hash((self.n, self.e, self.d, self.p, self.q, self.exp1, self.exp2, self.coef)) + + def _get_blinding_factor(self) -> int: + for _ in range(1000): + blind_r = rsa.randnum.randint(self.n - 1) + if rsa.prime.are_relatively_prime(self.n, blind_r): + return blind_r + raise RuntimeError('unable to find blinding factor') + + def blinded_decrypt(self, encrypted: int) -> int: """Decrypts the message using blinding to prevent side-channel attacks. :param encrypted: the encrypted message @@ -393,13 +431,13 @@ def blinded_decrypt(self, encrypted): :rtype: int """ - blind_r = rsa.randnum.randint(self.n - 1) + blind_r = self._get_blinding_factor() blinded = self.blind(encrypted, blind_r) # blind before decrypting decrypted = rsa.core.decrypt_int(blinded, self.d, self.n) return self.unblind(decrypted, blind_r) - def blinded_encrypt(self, message): + def blinded_encrypt(self, message: int) -> int: """Encrypts the message using blinding to prevent side-channel attacks. :param message: the message to encrypt @@ -409,17 +447,18 @@ def blinded_encrypt(self, message): :rtype: int """ - blind_r = rsa.randnum.randint(self.n - 1) + blind_r = self._get_blinding_factor() blinded = self.blind(message, blind_r) # blind before encrypting encrypted = rsa.core.encrypt_int(blinded, self.d, self.n) return self.unblind(encrypted, blind_r) @classmethod - def _load_pkcs1_der(cls, keyfile): + def _load_pkcs1_der(cls, keyfile: bytes) -> 'PrivateKey': """Loads a key in PKCS#1 DER format. :param keyfile: contents of a DER-encoded file that contains the private key. + :type keyfile: bytes :return: a PrivateKey object First let's construct a DER encoded key: @@ -456,13 +495,26 @@ def _load_pkcs1_der(cls, keyfile): if priv[0] != 0: raise ValueError('Unable to read this file, version %s != 0' % priv[0]) - as_ints = tuple(int(x) for x in priv[1:9]) - return cls(*as_ints) + as_ints = map(int, priv[1:6]) + key = cls(*as_ints) + + exp1, exp2, coef = map(int, priv[6:9]) + + if (key.exp1, key.exp2, key.coef) != (exp1, exp2, coef): + warnings.warn( + 'You have provided a malformed keyfile. Either the exponents ' + 'or the coefficient are incorrect. Using the correct values ' + 'instead.', + UserWarning, + ) + + return key - def _save_pkcs1_der(self): + def _save_pkcs1_der(self) -> bytes: """Saves the private key in PKCS#1 DER format. - @returns: the DER-encoded private key. + :returns: the DER-encoded private key. + :rtype: bytes """ from pyasn1.type import univ, namedtype @@ -470,15 +522,15 @@ def _save_pkcs1_der(self): class AsnPrivKey(univ.Sequence): componentType = namedtype.NamedTypes( - namedtype.NamedType('version', univ.Integer()), - namedtype.NamedType('modulus', univ.Integer()), - namedtype.NamedType('publicExponent', univ.Integer()), - namedtype.NamedType('privateExponent', univ.Integer()), - namedtype.NamedType('prime1', univ.Integer()), - namedtype.NamedType('prime2', univ.Integer()), - namedtype.NamedType('exponent1', univ.Integer()), - namedtype.NamedType('exponent2', univ.Integer()), - namedtype.NamedType('coefficient', univ.Integer()), + namedtype.NamedType('version', univ.Integer()), + namedtype.NamedType('modulus', univ.Integer()), + namedtype.NamedType('publicExponent', univ.Integer()), + namedtype.NamedType('privateExponent', univ.Integer()), + namedtype.NamedType('prime1', univ.Integer()), + namedtype.NamedType('prime2', univ.Integer()), + namedtype.NamedType('exponent1', univ.Integer()), + namedtype.NamedType('exponent2', univ.Integer()), + namedtype.NamedType('coefficient', univ.Integer()), ) # Create the ASN object @@ -496,7 +548,7 @@ class AsnPrivKey(univ.Sequence): return encoder.encode(asn_key) @classmethod - def _load_pkcs1_pem(cls, keyfile): + def _load_pkcs1_pem(cls, keyfile: bytes) -> 'PrivateKey': """Loads a PKCS#1 PEM-encoded private key file. The contents of the file before the "-----BEGIN RSA PRIVATE KEY-----" and @@ -504,23 +556,27 @@ def _load_pkcs1_pem(cls, keyfile): :param keyfile: contents of a PEM-encoded file that contains the private key. + :type keyfile: bytes :return: a PrivateKey object """ - der = rsa.pem.load_pem(keyfile, b('RSA PRIVATE KEY')) + der = rsa.pem.load_pem(keyfile, b'RSA PRIVATE KEY') return cls._load_pkcs1_der(der) - def _save_pkcs1_pem(self): + def _save_pkcs1_pem(self) -> bytes: """Saves a PKCS#1 PEM-encoded private key file. :return: contents of a PEM-encoded file that contains the private key. + :rtype: bytes """ der = self._save_pkcs1_der() - return rsa.pem.save_pem(der, b('RSA PRIVATE KEY')) + return rsa.pem.save_pem(der, b'RSA PRIVATE KEY') -def find_p_q(nbits, getprime_func=rsa.prime.getprime, accurate=True): +def find_p_q(nbits: int, + getprime_func: typing.Callable[[int], int] = rsa.prime.getprime, + accurate: bool = True) -> typing.Tuple[int, int]: """Returns a tuple of two different primes of nbits bits each. The resulting p * q has exacty 2 * nbits bits, and the returned p and q @@ -565,7 +621,7 @@ def find_p_q(nbits, getprime_func=rsa.prime.getprime, accurate=True): log.debug('find_p_q(%i): Finding q', nbits) q = getprime_func(qbits) - def is_acceptable(p, q): + def is_acceptable(p: int, q: int) -> bool: """Returns True iff p and q are acceptable: - p and q differ @@ -598,7 +654,7 @@ def is_acceptable(p, q): return max(p, q), min(p, q) -def calculate_keys_custom_exponent(p, q, exponent): +def calculate_keys_custom_exponent(p: int, q: int, exponent: int) -> typing.Tuple[int, int]: """Calculates an encryption and a decryption key given p, q and an exponent, and returns them as a tuple (e, d) @@ -615,9 +671,11 @@ def calculate_keys_custom_exponent(p, q, exponent): try: d = rsa.common.inverse(exponent, phi_n) - except ValueError: - raise ValueError("e (%d) and phi_n (%d) are not relatively prime" % - (exponent, phi_n)) + except rsa.common.NotRelativePrimeError as ex: + raise rsa.common.NotRelativePrimeError( + exponent, phi_n, ex.d, + msg="e (%d) and phi_n (%d) are not relatively prime (divider=%i)" % + (exponent, phi_n, ex.d)) if (exponent * d) % phi_n != 1: raise ValueError("e (%d) and d (%d) are not mult. inv. modulo " @@ -626,7 +684,7 @@ def calculate_keys_custom_exponent(p, q, exponent): return exponent, d -def calculate_keys(p, q): +def calculate_keys(p: int, q: int) -> typing.Tuple[int, int]: """Calculates an encryption and a decryption key given p and q, and returns them as a tuple (e, d) @@ -639,7 +697,10 @@ def calculate_keys(p, q): return calculate_keys_custom_exponent(p, q, DEFAULT_EXPONENT) -def gen_keys(nbits, getprime_func, accurate=True, exponent=DEFAULT_EXPONENT): +def gen_keys(nbits: int, + getprime_func: typing.Callable[[int], int], + accurate: bool = True, + exponent: int = DEFAULT_EXPONENT) -> typing.Tuple[int, int, int, int]: """Generate RSA keys of nbits bits. Returns (p, q, e, d). Note: this can take a long time, depending on the key size. @@ -667,7 +728,10 @@ def gen_keys(nbits, getprime_func, accurate=True, exponent=DEFAULT_EXPONENT): return p, q, e, d -def newkeys(nbits, accurate=True, poolsize=1, exponent=DEFAULT_EXPONENT): +def newkeys(nbits: int, + accurate: bool = True, + poolsize: int = 1, + exponent: int = DEFAULT_EXPONENT) -> typing.Tuple[PublicKey, PrivateKey]: """Generates public and private keys, and returns them as (pub, priv). The public key is also known as the 'encryption key', and is a @@ -702,9 +766,9 @@ def newkeys(nbits, accurate=True, poolsize=1, exponent=DEFAULT_EXPONENT): # Determine which getprime function to use if poolsize > 1: from rsa import parallel - import functools - getprime_func = functools.partial(parallel.getprime, poolsize=poolsize) + def getprime_func(nbits: int) -> int: + return parallel.getprime(nbits, poolsize=poolsize) else: getprime_func = rsa.prime.getprime @@ -731,7 +795,7 @@ def newkeys(nbits, accurate=True, poolsize=1, exponent=DEFAULT_EXPONENT): if failures: break - if (count and count % 10 == 0) or count == 1: + if (count % 10 == 0 and count) or count == 1: print('%i times' % count) except KeyboardInterrupt: print('Aborted') diff --git a/rsa/parallel.py b/rsa/parallel.py index edc924f..f9afedb 100644 --- a/rsa/parallel.py +++ b/rsa/parallel.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,15 +22,14 @@ """ -from __future__ import print_function - import multiprocessing as mp +from multiprocessing.connection import Connection import rsa.prime import rsa.randnum -def _find_prime(nbits, pipe): +def _find_prime(nbits: int, pipe: Connection) -> None: while True: integer = rsa.randnum.read_random_odd_int(nbits) @@ -42,7 +39,7 @@ def _find_prime(nbits, pipe): return -def getprime(nbits, poolsize): +def getprime(nbits: int, poolsize: int) -> int: """Returns a prime number that can be stored in 'nbits' bits. Works in multiple threads at the same time. @@ -94,7 +91,7 @@ def getprime(nbits, poolsize): if failures: break - if count and count % 10 == 0: + if count % 10 == 0 and count: print('%i times' % count) print('Doctests done') diff --git a/rsa/pem.py b/rsa/pem.py index 0f68cb2..1ffb446 100644 --- a/rsa/pem.py +++ b/rsa/pem.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,44 +15,29 @@ """Functions that load and write PEM-encoded files.""" import base64 -from rsa._compat import b, is_bytes +import typing + +# Should either be ASCII strings or bytes. +FlexiText = typing.Union[str, bytes] -def _markers(pem_marker): +def _markers(pem_marker: FlexiText) -> typing.Tuple[bytes, bytes]: """ - Returns the start and end PEM markers + Returns the start and end PEM markers, as bytes. """ - if is_bytes(pem_marker): - pem_marker = pem_marker.decode('utf-8') - - return (b('-----BEGIN %s-----' % pem_marker), - b('-----END %s-----' % pem_marker)) - - -def load_pem(contents, pem_marker): - """Loads a PEM file. - - :param contents: the contents of the file to interpret - :param pem_marker: the marker of the PEM content, such as 'RSA PRIVATE KEY' - when your file has '-----BEGIN RSA PRIVATE KEY-----' and - '-----END RSA PRIVATE KEY-----' markers. - - :return: the base64-decoded content between the start and end markers. - - @raise ValueError: when the content is invalid, for example when the start - marker cannot be found. + if not isinstance(pem_marker, bytes): + pem_marker = pem_marker.encode('ascii') - """ + return (b'-----BEGIN ' + pem_marker + b'-----', + b'-----END ' + pem_marker + b'-----') - # We want bytes, not text. If it's text, it can be converted to ASCII bytes. - if not is_bytes(contents): - contents = contents.encode('ascii') - (pem_start, pem_end) = _markers(pem_marker) +def _pem_lines(contents: bytes, pem_start: bytes, pem_end: bytes) -> typing.Iterator[bytes]: + """Generator over PEM lines between pem_start and pem_end.""" - pem_lines = [] in_pem_part = False + seen_pem_start = False for line in contents.splitlines(): line = line.strip() @@ -66,9 +49,10 @@ def load_pem(contents, pem_marker): # Handle start marker if line == pem_start: if in_pem_part: - raise ValueError('Seen start marker "%s" twice' % pem_start) + raise ValueError('Seen start marker "%r" twice' % pem_start) in_pem_part = True + seen_pem_start = True continue # Skip stuff before first marker @@ -81,24 +65,47 @@ def load_pem(contents, pem_marker): break # Load fields - if b(':') in line: + if b':' in line: continue - pem_lines.append(line) + yield line # Do some sanity checks - if not pem_lines: - raise ValueError('No PEM start marker "%s" found' % pem_start) + if not seen_pem_start: + raise ValueError('No PEM start marker "%r" found' % pem_start) if in_pem_part: - raise ValueError('No PEM end marker "%s" found' % pem_end) + raise ValueError('No PEM end marker "%r" found' % pem_end) + + +def load_pem(contents: FlexiText, pem_marker: FlexiText) -> bytes: + """Loads a PEM file. + + :param contents: the contents of the file to interpret + :param pem_marker: the marker of the PEM content, such as 'RSA PRIVATE KEY' + when your file has '-----BEGIN RSA PRIVATE KEY-----' and + '-----END RSA PRIVATE KEY-----' markers. + + :return: the base64-decoded content between the start and end markers. + + @raise ValueError: when the content is invalid, for example when the start + marker cannot be found. + + """ + + # We want bytes, not text. If it's text, it can be converted to ASCII bytes. + if not isinstance(contents, bytes): + contents = contents.encode('ascii') + + (pem_start, pem_end) = _markers(pem_marker) + pem_lines = [line for line in _pem_lines(contents, pem_start, pem_end)] # Base64-decode the contents - pem = b('').join(pem_lines) + pem = b''.join(pem_lines) return base64.standard_b64decode(pem) -def save_pem(contents, pem_marker): +def save_pem(contents: bytes, pem_marker: FlexiText) -> bytes: """Saves a PEM file. :param contents: the contents to encode in PEM format @@ -106,13 +113,13 @@ def save_pem(contents, pem_marker): when your file has '-----BEGIN RSA PRIVATE KEY-----' and '-----END RSA PRIVATE KEY-----' markers. - :return: the base64-encoded content between the start and end markers. + :return: the base64-encoded content between the start and end markers, as bytes. """ (pem_start, pem_end) = _markers(pem_marker) - b64 = base64.standard_b64encode(contents).replace(b('\n'), b('')) + b64 = base64.standard_b64encode(contents).replace(b'\n', b'') pem_lines = [pem_start] for block_start in range(0, len(b64), 64): @@ -120,6 +127,6 @@ def save_pem(contents, pem_marker): pem_lines.append(block) pem_lines.append(pem_end) - pem_lines.append(b('')) + pem_lines.append(b'') - return b('\n').join(pem_lines) + return b'\n'.join(pem_lines) diff --git a/rsa/pkcs1.py b/rsa/pkcs1.py index 28f0dc5..408bc5b 100644 --- a/rsa/pkcs1.py +++ b/rsa/pkcs1.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,25 +28,40 @@ import hashlib import os +import sys +import typing + +from . import common, transform, core, key -from rsa._compat import b -from rsa import common, transform, core +if sys.version_info < (3, 6): + # Python 3.6 and newer have SHA-3 support. For Python 3.5 we need a third party library. + # This library monkey-patches the hashlib module so that it looks like Python actually + # supports SHA-3 natively. + import sha3 # noqa: F401 # ASN.1 codes that describe the hash algorithm used. HASH_ASN1 = { - 'MD5': b('\x30\x20\x30\x0c\x06\x08\x2a\x86\x48\x86\xf7\x0d\x02\x05\x05\x00\x04\x10'), - 'SHA-1': b('\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'), - 'SHA-256': b('\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20'), - 'SHA-384': b('\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30'), - 'SHA-512': b('\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40'), + 'MD5': b'\x30\x20\x30\x0c\x06\x08\x2a\x86\x48\x86\xf7\x0d\x02\x05\x05\x00\x04\x10', + 'SHA-1': b'\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14', + 'SHA-224': b'\x30\x2d\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x04\x05\x00\x04\x1c', + 'SHA-256': b'\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20', + 'SHA-384': b'\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30', + 'SHA-512': b'\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40', + 'SHA3-256': b'\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x08\x05\x00\x04\x20', + 'SHA3-384': b'\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x09\x05\x00\x04\x30', + 'SHA3-512': b'\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x0a\x05\x00\x04\x40', } HASH_METHODS = { 'MD5': hashlib.md5, 'SHA-1': hashlib.sha1, + 'SHA-224': hashlib.sha224, 'SHA-256': hashlib.sha256, 'SHA-384': hashlib.sha384, 'SHA-512': hashlib.sha512, + 'SHA3-256': hashlib.sha3_256, + 'SHA3-384': hashlib.sha3_384, + 'SHA3-512': hashlib.sha3_512, } @@ -64,7 +77,7 @@ class VerificationError(CryptoError): """Raised when verification fails.""" -def _pad_for_encryption(message, target_length): +def _pad_for_encryption(message: bytes, target_length: int) -> bytes: r"""Pads the message for encryption, returning the padded message. :return: 00 02 RANDOM_DATA 00 MESSAGE @@ -87,7 +100,7 @@ def _pad_for_encryption(message, target_length): ' space for %i' % (msglength, max_msglength)) # Get random padding - padding = b('') + padding = b'' padding_length = target_length - msglength - 3 # We remove 0-bytes, so we'll end up with less padding than we've asked for, @@ -99,18 +112,18 @@ def _pad_for_encryption(message, target_length): # after removing the 0-bytes. This increases the chance of getting # enough bytes, especially when needed_bytes is small new_padding = os.urandom(needed_bytes + 5) - new_padding = new_padding.replace(b('\x00'), b('')) + new_padding = new_padding.replace(b'\x00', b'') padding = padding + new_padding[:needed_bytes] assert len(padding) == padding_length - return b('').join([b('\x00\x02'), - padding, - b('\x00'), - message]) + return b''.join([b'\x00\x02', + padding, + b'\x00', + message]) -def _pad_for_signing(message, target_length): +def _pad_for_signing(message: bytes, target_length: int) -> bytes: r"""Pads the message for signing, returning the padded message. The padding is always a repetition of FF bytes. @@ -138,13 +151,13 @@ def _pad_for_signing(message, target_length): padding_length = target_length - msglength - 3 - return b('').join([b('\x00\x01'), - padding_length * b('\xff'), - b('\x00'), - message]) + return b''.join([b'\x00\x01', + padding_length * b'\xff', + b'\x00', + message]) -def encrypt(message, pub_key): +def encrypt(message: bytes, pub_key: key.PublicKey) -> bytes: """Encrypts the given message using PKCS#1 v1.5 :param message: the message to encrypt. Must be a byte string no longer than @@ -176,7 +189,7 @@ def encrypt(message, pub_key): return block -def decrypt(crypto, priv_key): +def decrypt(crypto: bytes, priv_key: key.PrivateKey) -> bytes: r"""Decrypts the given message using PKCS#1 v1.5 The decryption is considered 'failed' when the resulting cleartext doesn't @@ -232,31 +245,35 @@ def decrypt(crypto, priv_key): decrypted = priv_key.blinded_decrypt(encrypted) cleartext = transform.int2bytes(decrypted, blocksize) + # Detect leading zeroes in the crypto. These are not reflected in the + # encrypted value (as leading zeroes do not influence the value of an + # integer). This fixes CVE-2020-13757. + if len(crypto) > blocksize: + raise DecryptionError('Decryption failed') + # If we can't find the cleartext marker, decryption failed. - if cleartext[0:2] != b('\x00\x02'): + if cleartext[0:2] != b'\x00\x02': raise DecryptionError('Decryption failed') # Find the 00 separator between the padding and the message try: - sep_idx = cleartext.index(b('\x00'), 2) + sep_idx = cleartext.index(b'\x00', 2) except ValueError: raise DecryptionError('Decryption failed') return cleartext[sep_idx + 1:] -def sign(message, priv_key, hash): - """Signs the message with the private key. +def sign_hash(hash_value: bytes, priv_key: key.PrivateKey, hash_method: str) -> bytes: + """Signs a precomputed hash with the private key. Hashes the message, then signs the hash with the given key. This is known as a "detached signature", because the message itself isn't altered. - :param message: the message to sign. Can be an 8-bit string or a file-like - object. If ``message`` has a ``read()`` method, it is assumed to be a - file-like object. + :param hash_value: A precomputed hash to sign (ignores message). :param priv_key: the :py:class:`rsa.PrivateKey` to sign with - :param hash: the hash method used on the message. Use 'MD5', 'SHA-1', - 'SHA-256', 'SHA-384' or 'SHA-512'. + :param hash_method: the hash method used on the message. Use 'MD5', 'SHA-1', + 'SHA-224', SHA-256', 'SHA-384' or 'SHA-512'. :return: a message signature block. :raise OverflowError: if the private key is too small to contain the requested hash. @@ -264,15 +281,12 @@ def sign(message, priv_key, hash): """ # Get the ASN1 code for this hash method - if hash not in HASH_ASN1: - raise ValueError('Invalid hash method: %s' % hash) - asn1code = HASH_ASN1[hash] - - # Calculate the hash - hash = _hash(message, hash) + if hash_method not in HASH_ASN1: + raise ValueError('Invalid hash method: %s' % hash_method) + asn1code = HASH_ASN1[hash_method] # Encrypt the hash with the private key - cleartext = asn1code + hash + cleartext = asn1code + hash_value keylength = common.byte_size(priv_key.n) padded = _pad_for_signing(cleartext, keylength) @@ -283,7 +297,29 @@ def sign(message, priv_key, hash): return block -def verify(message, signature, pub_key): +def sign(message: bytes, priv_key: key.PrivateKey, hash_method: str) -> bytes: + """Signs the message with the private key. + + Hashes the message, then signs the hash with the given key. This is known + as a "detached signature", because the message itself isn't altered. + + :param message: the message to sign. Can be an 8-bit string or a file-like + object. If ``message`` has a ``read()`` method, it is assumed to be a + file-like object. + :param priv_key: the :py:class:`rsa.PrivateKey` to sign with + :param hash_method: the hash method used on the message. Use 'MD5', 'SHA-1', + 'SHA-224', SHA-256', 'SHA-384' or 'SHA-512'. + :return: a message signature block. + :raise OverflowError: if the private key is too small to contain the + requested hash. + + """ + + msg_hash = compute_hash(message, hash_method) + return sign_hash(msg_hash, priv_key, hash_method) + + +def verify(message: bytes, signature: bytes, pub_key: key.PublicKey) -> str: """Verifies that the signature matches the message. The hash method is detected automatically from the signature. @@ -294,6 +330,7 @@ def verify(message, signature, pub_key): :param signature: the signature block, as created with :py:func:`rsa.sign`. :param pub_key: the :py:class:`rsa.PublicKey` of the person signing the message. :raise VerificationError: when the signature doesn't match the message. + :returns: the name of the used hash. """ @@ -304,20 +341,63 @@ def verify(message, signature, pub_key): # Get the hash method method_name = _find_method_hash(clearsig) - message_hash = _hash(message, method_name) + message_hash = compute_hash(message, method_name) # Reconstruct the expected padded hash cleartext = HASH_ASN1[method_name] + message_hash expected = _pad_for_signing(cleartext, keylength) + if len(signature) != keylength: + raise VerificationError('Verification failed') + # Compare with the signed one if expected != clearsig: raise VerificationError('Verification failed') - return True + return method_name -def _hash(message, method_name): +def find_signature_hash(signature: bytes, pub_key: key.PublicKey) -> str: + """Returns the hash name detected from the signature. + + If you also want to verify the message, use :py:func:`rsa.verify()` instead. + It also returns the name of the used hash. + + :param signature: the signature block, as created with :py:func:`rsa.sign`. + :param pub_key: the :py:class:`rsa.PublicKey` of the person signing the message. + :returns: the name of the used hash. + """ + + keylength = common.byte_size(pub_key.n) + encrypted = transform.bytes2int(signature) + decrypted = core.decrypt_int(encrypted, pub_key.e, pub_key.n) + clearsig = transform.int2bytes(decrypted, keylength) + + return _find_method_hash(clearsig) + + +def yield_fixedblocks(infile: typing.BinaryIO, blocksize: int) -> typing.Iterator[bytes]: + """Generator, yields each block of ``blocksize`` bytes in the input file. + + :param infile: file to read and separate in blocks. + :param blocksize: block size in bytes. + :returns: a generator that yields the contents of each block + """ + + while True: + block = infile.read(blocksize) + + read_bytes = len(block) + if read_bytes == 0: + break + + yield block + + if read_bytes < blocksize: + break + + +def compute_hash(message: typing.Union[bytes, typing.BinaryIO], method_name: str) -> bytes: """Returns the message digest. :param message: the signed message. Can be an 8-bit string or a file-like @@ -334,21 +414,18 @@ def _hash(message, method_name): method = HASH_METHODS[method_name] hasher = method() - if hasattr(message, 'read') and hasattr(message.read, '__call__'): - # Late import to prevent DeprecationWarnings. - from . import varblock - + if isinstance(message, bytes): + hasher.update(message) + else: + assert hasattr(message, 'read') and hasattr(message.read, '__call__') # read as 1K blocks - for block in varblock.yield_fixedblocks(message, 1024): + for block in yield_fixedblocks(message, 1024): hasher.update(block) - else: - # hash the message object itself. - hasher.update(message) return hasher.digest() -def _find_method_hash(clearsig): +def _find_method_hash(clearsig: bytes) -> str: """Finds the hash method. :param clearsig: full padded ASN1 and hash. @@ -375,7 +452,7 @@ def _find_method_hash(clearsig): if failures: break - if count and count % 100 == 0: + if count % 100 == 0 and count: print('%i times' % count) print('Doctests done') diff --git a/rsa/pkcs1_v2.py b/rsa/pkcs1_v2.py new file mode 100644 index 0000000..f780aff --- /dev/null +++ b/rsa/pkcs1_v2.py @@ -0,0 +1,100 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions for PKCS#1 version 2 encryption and signing + +This module implements certain functionality from PKCS#1 version 2. Main +documentation is RFC 2437: https://tools.ietf.org/html/rfc2437 +""" + +from rsa import ( + common, + pkcs1, + transform, +) + + +def mgf1(seed: bytes, length: int, hasher: str = 'SHA-1') -> bytes: + """ + MGF1 is a Mask Generation Function based on a hash function. + + A mask generation function takes an octet string of variable length and a + desired output length as input, and outputs an octet string of the desired + length. The plaintext-awareness of RSAES-OAEP relies on the random nature of + the output of the mask generation function, which in turn relies on the + random nature of the underlying hash. + + :param bytes seed: seed from which mask is generated, an octet string + :param int length: intended length in octets of the mask, at most 2^32(hLen) + :param str hasher: hash function (hLen denotes the length in octets of the hash + function output) + + :return: mask, an octet string of length `length` + :rtype: bytes + + :raise OverflowError: when `length` is too large for the specified `hasher` + :raise ValueError: when specified `hasher` is invalid + """ + + try: + hash_length = pkcs1.HASH_METHODS[hasher]().digest_size + except KeyError: + raise ValueError( + 'Invalid `hasher` specified. Please select one of: {hash_list}'.format( + hash_list=', '.join(sorted(pkcs1.HASH_METHODS.keys())) + ) + ) + + # If l > 2^32(hLen), output "mask too long" and stop. + if length > (2**32 * hash_length): + raise OverflowError( + "Desired length should be at most 2**32 times the hasher's output " + "length ({hash_length} for {hasher} function)".format( + hash_length=hash_length, + hasher=hasher, + ) + ) + + # Looping `counter` from 0 to ceil(l / hLen)-1, build `output` based on the + # hashes formed by (`seed` + C), being `C` an octet string of length 4 + # generated by converting `counter` with the primitive I2OSP + output = b''.join( + pkcs1.compute_hash( + seed + transform.int2bytes(counter, fill_size=4), + method_name=hasher, + ) + for counter in range(common.ceil_div(length, hash_length) + 1) + ) + + # Output the leading `length` octets of `output` as the octet string mask. + return output[:length] + + +__all__ = [ + 'mgf1', +] + +if __name__ == '__main__': + print('Running doctests 1000x or until failure') + import doctest + + for count in range(1000): + (failures, tests) = doctest.testmod() + if failures: + break + + if count % 100 == 0 and count: + print('%i times' % count) + + print('Doctests done') diff --git a/rsa/prime.py b/rsa/prime.py index 6f23f9d..853aca5 100644 --- a/rsa/prime.py +++ b/rsa/prime.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,12 +18,13 @@ Roberto Tamassia, 2002. """ +import rsa.common import rsa.randnum __all__ = ['getprime', 'are_relatively_prime'] -def gcd(p, q): +def gcd(p: int, q: int) -> int: """Returns the greatest common divisor of p and q >>> gcd(48, 180) @@ -37,7 +36,33 @@ def gcd(p, q): return p -def miller_rabin_primality_testing(n, k): +def get_primality_testing_rounds(number: int) -> int: + """Returns minimum number of rounds for Miller-Rabing primality testing, + based on number bitsize. + + According to NIST FIPS 186-4, Appendix C, Table C.3, minimum number of + rounds of M-R testing, using an error probability of 2 ** (-100), for + different p, q bitsizes are: + * p, q bitsize: 512; rounds: 7 + * p, q bitsize: 1024; rounds: 4 + * p, q bitsize: 1536; rounds: 3 + See: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf + """ + + # Calculate number bitsize. + bitsize = rsa.common.bit_size(number) + # Set number of rounds. + if bitsize >= 1536: + return 3 + if bitsize >= 1024: + return 4 + if bitsize >= 512: + return 7 + # For smaller bitsizes, set arbitrary number of rounds. + return 10 + + +def miller_rabin_primality_testing(n: int, k: int) -> bool: """Calculates whether n is composite (which is always correct) or prime (which theoretically is incorrect with error probability 4**-k), by applying Miller-Rabin primality testing. @@ -69,7 +94,7 @@ def miller_rabin_primality_testing(n, k): # Test k witnesses. for _ in range(k): # Generate random integer a, where 2 <= a <= (n - 2) - a = rsa.randnum.randint(n - 4) + 2 + a = rsa.randnum.randint(n - 3) + 1 x = pow(a, d, n) if x == 1 or x == n - 1: @@ -90,7 +115,7 @@ def miller_rabin_primality_testing(n, k): return True -def is_prime(number): +def is_prime(number: int) -> bool: """Returns True if the number is prime, and False otherwise. >>> is_prime(2) @@ -99,29 +124,24 @@ def is_prime(number): False >>> is_prime(41) True - >>> [x for x in range(901, 1000) if is_prime(x)] - [907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997] """ # Check for small numbers. if number < 10: - return number in [2, 3, 5, 7] + return number in {2, 3, 5, 7} # Check for even numbers. if not (number & 1): return False - # According to NIST FIPS 186-4, Appendix C, Table C.3, minimum number of - # rounds of M-R testing, using an error probability of 2 ** (-100), for - # different p, q bitsizes are: - # * p, q bitsize: 512; rounds: 7 - # * p, q bitsize: 1024; rounds: 4 - # * p, q bitsize: 1536; rounds: 3 - # See: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf - return miller_rabin_primality_testing(number, 7) + # Calculate minimum number of rounds. + k = get_primality_testing_rounds(number) + + # Run primality testing with (minimum + 1) rounds. + return miller_rabin_primality_testing(number, k + 1) -def getprime(nbits): +def getprime(nbits: int) -> int: """Returns a prime number that can be stored in 'nbits' bits. >>> p = getprime(128) @@ -149,7 +169,7 @@ def getprime(nbits): # Retry if not prime -def are_relatively_prime(a, b): +def are_relatively_prime(a: int, b: int) -> bool: """Returns True if a and b are relatively prime, and False if they are not. @@ -172,7 +192,7 @@ def are_relatively_prime(a, b): if failures: break - if count and count % 100 == 0: + if count % 100 == 0 and count: print('%i times' % count) print('Doctests done') diff --git a/rsa/randnum.py b/rsa/randnum.py index 3c788a5..a5bb850 100644 --- a/rsa/randnum.py +++ b/rsa/randnum.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,12 +17,12 @@ # Source inspired by code by Yesudeep Mangalapilly import os +import struct from rsa import common, transform -from rsa._compat import byte -def read_random_bits(nbits): +def read_random_bits(nbits: int) -> bytes: """Reads 'nbits' random bits. If nbits isn't a whole number of bytes, an extra byte will be appended with @@ -40,12 +38,12 @@ def read_random_bits(nbits): if rbits > 0: randomvalue = ord(os.urandom(1)) randomvalue >>= (8 - rbits) - randomdata = byte(randomvalue) + randomdata + randomdata = struct.pack("B", randomvalue) + randomdata return randomdata -def read_random_int(nbits): +def read_random_int(nbits: int) -> int: """Reads a random integer of approximately nbits bits. """ @@ -59,7 +57,7 @@ def read_random_int(nbits): return value -def read_random_odd_int(nbits): +def read_random_odd_int(nbits: int) -> int: """Reads a random odd integer of approximately nbits bits. >>> read_random_odd_int(512) & 1 @@ -72,7 +70,7 @@ def read_random_odd_int(nbits): return value | 1 -def randint(maxvalue): +def randint(maxvalue: int) -> int: """Returns a random integer x with 1 <= x <= maxvalue May take a very long time in specific situations. If maxvalue needs N bits @@ -88,7 +86,7 @@ def randint(maxvalue): if value <= maxvalue: break - if tries and tries % 10 == 0: + 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. diff --git a/rsa/transform.py b/rsa/transform.py index 16061a9..03c4a77 100644 --- a/rsa/transform.py +++ b/rsa/transform.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,25 +17,10 @@ From bytes to a number, number to bytes, etc. """ -from __future__ import absolute_import - -try: - # We'll use psyco if available on 32-bit architectures to speed up code. - # Using psyco (if available) cuts down the execution time on Python 2.5 - # at least by half. - import psyco - - psyco.full() -except ImportError: - pass +import math -import binascii -from struct import pack -from rsa import common -from rsa._compat import is_integer, b, byte, get_word_alignment, ZERO_BYTE, EMPTY_BYTE - -def bytes2int(raw_bytes): +def bytes2int(raw_bytes: bytes) -> int: r"""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. @@ -48,110 +31,14 @@ def bytes2int(raw_bytes): 8405007 """ + return int.from_bytes(raw_bytes, 'big', signed=False) - return int(binascii.hexlify(raw_bytes), 16) - - -def _int2bytes(number, block_size=None): - r"""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. +def int2bytes(number: int, fill_size: int = 0) -> bytes: """ + Convert an unsigned integer to bytes (big-endian):: - # 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 = [ZERO_BYTE] - 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) * ZERO_BYTE - else: - padding = EMPTY_BYTE - - return padding + EMPTY_BYTE.join(raw_bytes) - - -def bytes_leading(raw_bytes, needle=ZERO_BYTE): - """ - 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 \000. - :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. + Does not preserve leading zeros if you don't specify a fill size. :param number: Integer value @@ -159,15 +46,6 @@ def int2bytes(number, fill_size=None, chunk_size=None, overflow=False): 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: @@ -180,42 +58,12 @@ def int2bytes(number, fill_size=None, chunk_size=None, overflow=False): 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. - number & 1 - - raw_bytes = b('') - - # Pack the integer one machine word at a time into bytes. - num = number - word_bits, _, max_uint, pack_type = 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 = ZERO_BYTE - # 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 = raw_bytes.rjust(fill_size, ZERO_BYTE) - elif chunk_size and chunk_size > 0: - remainder = length % chunk_size - if remainder: - padding_size = chunk_size - remainder - raw_bytes = raw_bytes.rjust(length + padding_size, ZERO_BYTE) - return raw_bytes + bytes_required = max(1, math.ceil(number.bit_length() / 8)) + + if fill_size > 0: + return number.to_bytes(fill_size, 'big') + + return number.to_bytes(bytes_required, 'big') if __name__ == '__main__': diff --git a/rsa/util.py b/rsa/util.py index 29d5eb1..cb31c46 100644 --- a/rsa/util.py +++ b/rsa/util.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,15 +14,13 @@ """Utility functions.""" -from __future__ import with_statement, print_function - import sys from optparse import OptionParser import rsa.key -def private_to_public(): +def private_to_public() -> None: """Reads a private key and outputs the corresponding public key.""" # Parse the CLI options diff --git a/rsa/varblock.py b/rsa/varblock.py deleted file mode 100644 index 1c8d839..0000000 --- a/rsa/varblock.py +++ /dev/null @@ -1,179 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2011 Sybren A. Stüvel -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""VARBLOCK file support - -.. deprecated:: 3.4 - - The VARBLOCK format is NOT recommended for general use, has been deprecated since - Python-RSA 3.4, and will be removed in a future release. It's vulnerable to a - number of attacks: - - 1. decrypt/encrypt_bigfile() does not implement `Authenticated encryption`_ nor - uses MACs to verify messages before decrypting public key encrypted messages. - - 2. decrypt/encrypt_bigfile() does not use hybrid encryption (it uses plain RSA) - and has no method for chaining, so block reordering is possible. - - See `issue #19 on Github`_ for more information. - -.. _Authenticated encryption: https://en.wikipedia.org/wiki/Authenticated_encryption -.. _issue #19 on Github: https://github.com/sybrenstuvel/python-rsa/issues/13 - - -The VARBLOCK file format is as follows, where || denotes byte concatenation: - - FILE := VERSION || BLOCK || BLOCK ... - - BLOCK := LENGTH || DATA - - LENGTH := varint-encoded length of the subsequent data. Varint comes from - Google Protobuf, and encodes an integer into a variable number of bytes. - Each byte uses the 7 lowest bits to encode the value. The highest bit set - to 1 indicates the next byte is also part of the varint. The last byte will - have this bit set to 0. - -This file format is called the VARBLOCK format, in line with the varint format -used to denote the block sizes. - -""" - -import warnings - -from rsa._compat import byte, b - -ZERO_BYTE = b('\x00') -VARBLOCK_VERSION = 1 - -warnings.warn("The 'rsa.varblock' module was deprecated in Python-RSA version " - "3.4 due to security issues in the VARBLOCK format. See " - "https://github.com/sybrenstuvel/python-rsa/issues/13 for more information.", - DeprecationWarning) - - -def read_varint(infile): - """Reads a varint from the file. - - When the first byte to be read indicates EOF, (0, 0) is returned. When an - EOF occurs when at least one byte has been read, an EOFError exception is - raised. - - :param infile: the file-like object to read from. It should have a read() - method. - :returns: (varint, length), the read varint and the number of read bytes. - """ - - varint = 0 - read_bytes = 0 - - while True: - char = infile.read(1) - if len(char) == 0: - if read_bytes == 0: - return 0, 0 - raise EOFError('EOF while reading varint, value is %i so far' % - varint) - - byte = ord(char) - varint += (byte & 0x7F) << (7 * read_bytes) - - read_bytes += 1 - - if not byte & 0x80: - return varint, read_bytes - - -def write_varint(outfile, value): - """Writes a varint to a file. - - :param outfile: the file-like object to write to. It should have a write() - method. - :returns: the number of written bytes. - """ - - # there is a big difference between 'write the value 0' (this case) and - # 'there is nothing left to write' (the false-case of the while loop) - - if value == 0: - outfile.write(ZERO_BYTE) - return 1 - - written_bytes = 0 - while value > 0: - to_write = value & 0x7f - value >>= 7 - - if value > 0: - to_write |= 0x80 - - outfile.write(byte(to_write)) - written_bytes += 1 - - return written_bytes - - -def yield_varblocks(infile): - """Generator, yields each block in the input file. - - :param infile: file to read, is expected to have the VARBLOCK format as - described in the module's docstring. - @yields the contents of each block. - """ - - # Check the version number - first_char = infile.read(1) - if len(first_char) == 0: - raise EOFError('Unable to read VARBLOCK version number') - - version = ord(first_char) - if version != VARBLOCK_VERSION: - raise ValueError('VARBLOCK version %i not supported' % version) - - while True: - (block_size, read_bytes) = read_varint(infile) - - # EOF at block boundary, that's fine. - if read_bytes == 0 and block_size == 0: - break - - block = infile.read(block_size) - - read_size = len(block) - if read_size != block_size: - raise EOFError('Block size is %i, but could read only %i bytes' % - (block_size, read_size)) - - yield block - - -def yield_fixedblocks(infile, blocksize): - """Generator, yields each block of ``blocksize`` bytes in the input file. - - :param infile: file to read and separate in blocks. - :returns: a generator that yields the contents of each block - """ - - while True: - block = infile.read(blocksize) - - read_bytes = len(block) - if read_bytes == 0: - break - - yield block - - if read_bytes < blocksize: - break diff --git a/setup.cfg b/setup.cfg index a38d4c4..e377bdb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,19 @@ -[nosetests] -verbosity=2 - [bdist_wheel] universal = 1 + +[metadata] +license_file = LICENSE + +[flake8] +max-line-length = 100 +max-complexity = 10 + +[pep8] +max-line-length = 100 + +[mypy] +python_version = 3.7 +warn_unused_ignores = True +ignore_missing_imports = True +follow_imports = skip +incremental = True diff --git a/setup.py b/setup.py deleted file mode 100755 index cd73fe8..0000000 --- a/setup.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright 2011 Sybren A. Stüvel -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from setuptools import setup - -if __name__ == '__main__': - setup(name='rsa', - version='3.4.2', - description='Pure-Python RSA implementation', - author='Sybren A. Stuvel', - author_email='sybren@stuvel.eu', - maintainer='Sybren A. Stuvel', - maintainer_email='sybren@stuvel.eu', - url='https://stuvel.eu/rsa', - packages=['rsa'], - license='ASL 2', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Intended Audience :: Education', - 'Intended Audience :: Information Technology', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Topic :: Security :: Cryptography', - ], - install_requires=[ - 'pyasn1 >= 0.1.3', - ], - entry_points={'console_scripts': [ - 'pyrsa-priv2pub = rsa.util:private_to_public', - 'pyrsa-keygen = rsa.cli:keygen', - 'pyrsa-encrypt = rsa.cli:encrypt', - 'pyrsa-decrypt = rsa.cli:decrypt', - 'pyrsa-sign = rsa.cli:sign', - 'pyrsa-verify = rsa.cli:verify', - 'pyrsa-encrypt-bigfile = rsa.cli:encrypt_bigfile', - 'pyrsa-decrypt-bigfile = rsa.cli:decrypt_bigfile', - ]}, - - ) diff --git a/speed.sh b/speed.sh index 72cc9ad..e06d825 100755 --- a/speed.sh +++ b/speed.sh @@ -28,9 +28,7 @@ check_command() { python_versions=" pypy - python2.6 python2.7 - python3.3 python3.4 python3.5 " @@ -43,12 +41,3 @@ for version in $python_versions; do "$version" -mtimeit -s'from rsa.transform import _int2bytes; n = 1<<4096' '_int2bytes(n)' fi done - -echo "bit_size speed test" -for version in $python_versions; do - if check_command "$version"; then - echo "$version" - "$version" -mtimeit -s'from rsa.common import bit_size; n = 1<<4096' 'bit_size(n)' - "$version" -mtimeit -s'from rsa.common import _bit_size; n = 1<<4096' '_bit_size(n)' - fi -done diff --git a/tests/test_bigfile.py b/tests/test_bigfile.py deleted file mode 100644 index 70278dc..0000000 --- a/tests/test_bigfile.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2011 Sybren A. Stüvel -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests block operations.""" - -from rsa._compat import b - -try: - from StringIO import StringIO as BytesIO -except ImportError: - from io import BytesIO -import unittest - -import rsa -from rsa import bigfile, varblock, pkcs1 - - -class BigfileTest(unittest.TestCase): - def test_encrypt_decrypt_bigfile(self): - # Expected block size + 11 bytes padding - pub_key, priv_key = rsa.newkeys((6 + 11) * 8) - - # Encrypt the file - message = b('123456Sybren') - infile = BytesIO(message) - outfile = BytesIO() - - bigfile.encrypt_bigfile(infile, outfile, pub_key) - - # Test - crypto = outfile.getvalue() - - cryptfile = BytesIO(crypto) - clearfile = BytesIO() - - bigfile.decrypt_bigfile(cryptfile, clearfile, priv_key) - self.assertEquals(clearfile.getvalue(), message) - - # We have 2x6 bytes in the message, so that should result in two - # bigfile. - cryptfile.seek(0) - varblocks = list(varblock.yield_varblocks(cryptfile)) - self.assertEqual(2, len(varblocks)) - - def test_sign_verify_bigfile(self): - # Large enough to store MD5-sum and ASN.1 code for MD5 - pub_key, priv_key = rsa.newkeys((34 + 11) * 8) - - # Sign the file - msgfile = BytesIO(b('123456Sybren')) - signature = pkcs1.sign(msgfile, priv_key, 'MD5') - - # Check the signature - msgfile.seek(0) - self.assertTrue(pkcs1.verify(msgfile, signature, pub_key)) - - # Alter the message, re-check - msgfile = BytesIO(b('123456sybren')) - self.assertRaises(pkcs1.VerificationError, - pkcs1.verify, msgfile, signature, pub_key) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..1cd92c2 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,286 @@ +""" +Unit tests for CLI entry points. +""" + +from __future__ import print_function + +import functools +import io +import os +import sys +import typing +import unittest +from contextlib import contextmanager, redirect_stdout, redirect_stderr + +import rsa +import rsa.cli +import rsa.util + + +@contextmanager +def captured_output() -> typing.Generator: + """Captures output to stdout and stderr""" + + # According to mypy, we're not supposed to change buf_out.buffer. + # However, this is just a test, and it works, hence the 'type: ignore'. + buf_out = io.StringIO() + buf_out.buffer = io.BytesIO() # type: ignore + + buf_err = io.StringIO() + buf_err.buffer = io.BytesIO() # type: ignore + + with redirect_stdout(buf_out), redirect_stderr(buf_err): + yield buf_out, buf_err + + +def get_bytes_out(buf) -> bytes: + return buf.buffer.getvalue() + + +@contextmanager +def cli_args(*new_argv): + """Updates sys.argv[1:] for a single test.""" + + old_args = sys.argv[:] + sys.argv[1:] = [str(arg) for arg in new_argv] + + try: + yield + finally: + sys.argv[1:] = old_args + + +def remove_if_exists(fname): + """Removes a file if it exists.""" + + if os.path.exists(fname): + os.unlink(fname) + + +def cleanup_files(*filenames): + """Makes sure the files don't exist when the test runs, and deletes them afterward.""" + + def remove(): + for fname in filenames: + remove_if_exists(fname) + + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + remove() + try: + return func(*args, **kwargs) + finally: + remove() + + return wrapper + + return decorator + + +class AbstractCliTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Ensure there is a key to use + cls.pub_key, cls.priv_key = rsa.newkeys(512) + cls.pub_fname = '%s.pub' % cls.__name__ + cls.priv_fname = '%s.key' % cls.__name__ + + with open(cls.pub_fname, 'wb') as outfile: + outfile.write(cls.pub_key.save_pkcs1()) + + with open(cls.priv_fname, 'wb') as outfile: + outfile.write(cls.priv_key.save_pkcs1()) + + @classmethod + def tearDownClass(cls): + if hasattr(cls, 'pub_fname'): + remove_if_exists(cls.pub_fname) + if hasattr(cls, 'priv_fname'): + remove_if_exists(cls.priv_fname) + + def assertExits(self, status_code, func, *args, **kwargs): + try: + func(*args, **kwargs) + except SystemExit as ex: + if status_code == ex.code: + return + self.fail('SystemExit() raised by %r, but exited with code %r, expected %r' % ( + func, ex.code, status_code)) + else: + self.fail('SystemExit() not raised by %r' % func) + + +class KeygenTest(AbstractCliTest): + def test_keygen_no_args(self): + with cli_args(): + self.assertExits(1, rsa.cli.keygen) + + def test_keygen_priv_stdout(self): + with captured_output() as (out, err): + with cli_args(128): + rsa.cli.keygen() + + lines = get_bytes_out(out).splitlines() + self.assertEqual(b'-----BEGIN RSA PRIVATE KEY-----', lines[0]) + self.assertEqual(b'-----END RSA PRIVATE KEY-----', lines[-1]) + + # The key size should be shown on stderr + self.assertTrue('128-bit key' in err.getvalue()) + + @cleanup_files('test_cli_privkey_out.pem') + def test_keygen_priv_out_pem(self): + with captured_output() as (out, err): + with cli_args('--out=test_cli_privkey_out.pem', '--form=PEM', 128): + rsa.cli.keygen() + + # The key size should be shown on stderr + self.assertTrue('128-bit key' in err.getvalue()) + + # The output file should be shown on stderr + self.assertTrue('test_cli_privkey_out.pem' in err.getvalue()) + + # If we can load the file as PEM, it's good enough. + with open('test_cli_privkey_out.pem', 'rb') as pemfile: + rsa.PrivateKey.load_pkcs1(pemfile.read()) + + @cleanup_files('test_cli_privkey_out.der') + def test_keygen_priv_out_der(self): + with captured_output() as (out, err): + with cli_args('--out=test_cli_privkey_out.der', '--form=DER', 128): + rsa.cli.keygen() + + # The key size should be shown on stderr + self.assertTrue('128-bit key' in err.getvalue()) + + # The output file should be shown on stderr + self.assertTrue('test_cli_privkey_out.der' in err.getvalue()) + + # If we can load the file as der, it's good enough. + with open('test_cli_privkey_out.der', 'rb') as derfile: + rsa.PrivateKey.load_pkcs1(derfile.read(), format='DER') + + @cleanup_files('test_cli_privkey_out.pem', 'test_cli_pubkey_out.pem') + def test_keygen_pub_out_pem(self): + with captured_output() as (out, err): + with cli_args('--out=test_cli_privkey_out.pem', + '--pubout=test_cli_pubkey_out.pem', + '--form=PEM', 256): + rsa.cli.keygen() + + # The key size should be shown on stderr + self.assertTrue('256-bit key' in err.getvalue()) + + # The output files should be shown on stderr + self.assertTrue('test_cli_privkey_out.pem' in err.getvalue()) + self.assertTrue('test_cli_pubkey_out.pem' in err.getvalue()) + + # If we can load the file as PEM, it's good enough. + with open('test_cli_pubkey_out.pem', 'rb') as pemfile: + rsa.PublicKey.load_pkcs1(pemfile.read()) + + +class EncryptDecryptTest(AbstractCliTest): + def test_empty_decrypt(self): + with cli_args(): + self.assertExits(1, rsa.cli.decrypt) + + def test_empty_encrypt(self): + with cli_args(): + self.assertExits(1, rsa.cli.encrypt) + + @cleanup_files('encrypted.txt', 'cleartext.txt') + def test_encrypt_decrypt(self): + with open('cleartext.txt', 'wb') as outfile: + outfile.write(b'Hello cleartext RSA users!') + + with cli_args('-i', 'cleartext.txt', '--out=encrypted.txt', self.pub_fname): + with captured_output(): + rsa.cli.encrypt() + + with cli_args('-i', 'encrypted.txt', self.priv_fname): + with captured_output() as (out, err): + rsa.cli.decrypt() + + # We should have the original cleartext on stdout now. + output = get_bytes_out(out) + self.assertEqual(b'Hello cleartext RSA users!', output) + + @cleanup_files('encrypted.txt', 'cleartext.txt') + def test_encrypt_decrypt_unhappy(self): + with open('cleartext.txt', 'wb') as outfile: + outfile.write(b'Hello cleartext RSA users!') + + with cli_args('-i', 'cleartext.txt', '--out=encrypted.txt', self.pub_fname): + with captured_output(): + rsa.cli.encrypt() + + # Change a few bytes in the encrypted stream. + with open('encrypted.txt', 'r+b') as encfile: + encfile.seek(40) + encfile.write(b'hahaha') + + with cli_args('-i', 'encrypted.txt', self.priv_fname): + with captured_output() as (out, err): + self.assertRaises(rsa.DecryptionError, rsa.cli.decrypt) + + +class SignVerifyTest(AbstractCliTest): + def test_empty_verify(self): + with cli_args(): + self.assertExits(1, rsa.cli.verify) + + def test_empty_sign(self): + with cli_args(): + self.assertExits(1, rsa.cli.sign) + + @cleanup_files('signature.txt', 'cleartext.txt') + def test_sign_verify(self): + with open('cleartext.txt', 'wb') as outfile: + outfile.write(b'Hello RSA users!') + + with cli_args('-i', 'cleartext.txt', '--out=signature.txt', self.priv_fname, 'SHA-256'): + with captured_output(): + rsa.cli.sign() + + with cli_args('-i', 'cleartext.txt', self.pub_fname, 'signature.txt'): + with captured_output() as (out, err): + rsa.cli.verify() + + self.assertFalse(b'Verification OK' in get_bytes_out(out)) + + @cleanup_files('signature.txt', 'cleartext.txt') + def test_sign_verify_unhappy(self): + with open('cleartext.txt', 'wb') as outfile: + outfile.write(b'Hello RSA users!') + + with cli_args('-i', 'cleartext.txt', '--out=signature.txt', self.priv_fname, 'SHA-256'): + with captured_output(): + rsa.cli.sign() + + # Change a few bytes in the cleartext file. + with open('cleartext.txt', 'r+b') as encfile: + encfile.seek(6) + encfile.write(b'DSA') + + with cli_args('-i', 'cleartext.txt', self.pub_fname, 'signature.txt'): + with captured_output() as (out, err): + self.assertExits('Verification failed.', rsa.cli.verify) + + +class PrivatePublicTest(AbstractCliTest): + """Test CLI command to convert a private to a public key.""" + + @cleanup_files('test_private_to_public.pem') + def test_private_to_public(self): + + with cli_args('-i', self.priv_fname, '-o', 'test_private_to_public.pem'): + with captured_output(): + rsa.util.private_to_public() + + # Check that the key is indeed valid. + with open('test_private_to_public.pem', 'rb') as pemfile: + key = rsa.PublicKey.load_pkcs1(pemfile.read()) + + self.assertEqual(self.priv_key.n, key.n) + self.assertEqual(self.priv_key.e, key.e) diff --git a/tests/test_common.py b/tests/test_common.py index 453dcc8..71b81d0 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,14 +15,14 @@ import unittest import struct -from rsa._compat import byte, b -from rsa.common import byte_size, bit_size, _bit_size +from rsa._compat import byte +from rsa.common import byte_size, bit_size, inverse class TestByte(unittest.TestCase): def test_values(self): - self.assertEqual(byte(0), b('\x00')) - self.assertEqual(byte(255), b('\xff')) + self.assertEqual(byte(0), b'\x00') + self.assertEqual(byte(255), b'\xff') def test_struct_error_when_out_of_bounds(self): self.assertRaises(struct.error, byte, 256) @@ -69,9 +67,28 @@ def test_values(self): self.assertEqual(bit_size((1 << 1024) + 1), 1025) self.assertEqual(bit_size((1 << 1024) - 1), 1024) - self.assertEqual(_bit_size(1023), 10) - self.assertEqual(_bit_size(1024), 11) - self.assertEqual(_bit_size(1025), 11) - self.assertEqual(_bit_size(1 << 1024), 1025) - self.assertEqual(_bit_size((1 << 1024) + 1), 1025) - self.assertEqual(_bit_size((1 << 1024) - 1), 1024) + def test_negative_values(self): + self.assertEqual(bit_size(-1023), 10) + self.assertEqual(bit_size(-1024), 11) + self.assertEqual(bit_size(-1025), 11) + self.assertEqual(bit_size(-1 << 1024), 1025) + self.assertEqual(bit_size(-((1 << 1024) + 1)), 1025) + self.assertEqual(bit_size(-((1 << 1024) - 1)), 1024) + + def test_bad_type(self): + self.assertRaises(TypeError, bit_size, []) + self.assertRaises(TypeError, bit_size, ()) + self.assertRaises(TypeError, bit_size, dict()) + self.assertRaises(TypeError, bit_size, "") + self.assertRaises(TypeError, bit_size, None) + self.assertRaises(TypeError, bit_size, 0.0) + + +class TestInverse(unittest.TestCase): + def test_normal(self): + self.assertEqual(3, inverse(7, 4)) + self.assertEqual(9, inverse(5, 11)) + + def test_not_relprime(self): + self.assertRaises(ValueError, inverse, 4, 8) + self.assertRaises(ValueError, inverse, 25, 5) diff --git a/tests/test_compat.py b/tests/test_compat.py index 8cbf101..e74f046 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,16 +15,64 @@ import unittest import struct -from rsa._compat import is_bytes, byte +from rsa._compat import byte, xor_bytes class TestByte(unittest.TestCase): + """Tests for single bytes.""" + def test_byte(self): for i in range(256): byt = byte(i) - self.assertTrue(is_bytes(byt)) + self.assertIsInstance(byt, bytes) self.assertEqual(ord(byt), i) def test_raises_StructError_on_overflow(self): self.assertRaises(struct.error, byte, 256) self.assertRaises(struct.error, byte, -1) + + def test_byte_literal(self): + self.assertIsInstance(b'abc', bytes) + + +class TestBytes(unittest.TestCase): + """Tests for bytes objects.""" + + def setUp(self): + self.b1 = b'\xff\xff\xff\xff' + self.b2 = b'\x00\x00\x00\x00' + self.b3 = b'\xf0\xf0\xf0\xf0' + self.b4 = b'\x4d\x23\xca\xe2' + self.b5 = b'\x9b\x61\x3b\xdc' + self.b6 = b'\xff\xff' + + self.byte_strings = (self.b1, self.b2, self.b3, self.b4, self.b5, self.b6) + + def test_xor_bytes(self): + self.assertEqual(xor_bytes(self.b1, self.b2), b'\xff\xff\xff\xff') + self.assertEqual(xor_bytes(self.b1, self.b3), b'\x0f\x0f\x0f\x0f') + self.assertEqual(xor_bytes(self.b1, self.b4), b'\xb2\xdc\x35\x1d') + self.assertEqual(xor_bytes(self.b1, self.b5), b'\x64\x9e\xc4\x23') + self.assertEqual(xor_bytes(self.b2, self.b3), b'\xf0\xf0\xf0\xf0') + self.assertEqual(xor_bytes(self.b2, self.b4), b'\x4d\x23\xca\xe2') + self.assertEqual(xor_bytes(self.b2, self.b5), b'\x9b\x61\x3b\xdc') + self.assertEqual(xor_bytes(self.b3, self.b4), b'\xbd\xd3\x3a\x12') + self.assertEqual(xor_bytes(self.b3, self.b5), b'\x6b\x91\xcb\x2c') + self.assertEqual(xor_bytes(self.b4, self.b5), b'\xd6\x42\xf1\x3e') + + def test_xor_bytes_length(self): + self.assertEqual(xor_bytes(self.b1, self.b6), b'\x00\x00') + self.assertEqual(xor_bytes(self.b2, self.b6), b'\xff\xff') + self.assertEqual(xor_bytes(self.b3, self.b6), b'\x0f\x0f') + self.assertEqual(xor_bytes(self.b4, self.b6), b'\xb2\xdc') + self.assertEqual(xor_bytes(self.b5, self.b6), b'\x64\x9e') + self.assertEqual(xor_bytes(self.b6, b''), b'') + + def test_xor_bytes_commutative(self): + for first in self.byte_strings: + for second in self.byte_strings: + min_length = min(len(first), len(second)) + result = xor_bytes(first, second) + + self.assertEqual(result, xor_bytes(second, first)) + self.assertEqual(len(result), min_length) diff --git a/tests/test_integers.py b/tests/test_integers.py index fb29ba4..2ca0a9a 100644 --- a/tests/test_integers.py +++ b/tests/test_integers.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_key.py b/tests/test_key.py index 0e62f55..9db30ce 100644 --- a/tests/test_key.py +++ b/tests/test_key.py @@ -40,3 +40,40 @@ def test_default_exponent(self): self.assertEqual(0x10001, priv.e) self.assertEqual(0x10001, pub.e) + + def test_exponents_coefficient_calculation(self): + pk = rsa.key.PrivateKey(3727264081, 65537, 3349121513, 65063, 57287) + + self.assertEqual(pk.exp1, 55063) + self.assertEqual(pk.exp2, 10095) + self.assertEqual(pk.coef, 50797) + + def test_custom_getprime_func(self): + # List of primes to test with, in order [p, q, p, q, ....] + # By starting with two of the same primes, we test that this is + # properly rejected. + primes = [64123, 64123, 64123, 50957, 39317, 33107] + + def getprime(_): + return primes.pop(0) + + # This exponent will cause two other primes to be generated. + exponent = 136407 + + (p, q, e, d) = rsa.key.gen_keys(64, + accurate=False, + getprime_func=getprime, + exponent=exponent) + self.assertEqual(39317, p) + self.assertEqual(33107, q) + + +class HashTest(unittest.TestCase): + """Test hashing of keys""" + + def test_hash_possible(self): + priv, pub = rsa.key.newkeys(16) + + # This raises a TypeError when hashing isn't possible. + hash(priv) + hash(pub) diff --git a/tests/test_load_save_keys.py b/tests/test_load_save_keys.py index 6f374cf..7892fb3 100644 --- a/tests/test_load_save_keys.py +++ b/tests/test_load_save_keys.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,62 +12,62 @@ # See the License for the specific language governing permissions and # limitations under the License. -'''Unittest for saving and loading keys.''' +"""Unittest for saving and loading keys.""" import base64 -import unittest import os.path import pickle - -from rsa._compat import b +import unittest +import warnings +from unittest import mock import rsa.key -B64PRIV_DER = b('MC4CAQACBQDeKYlRAgMBAAECBQDHn4npAgMA/icCAwDfxwIDANcXAgInbwIDAMZt') +B64PRIV_DER = b'MC4CAQACBQDeKYlRAgMBAAECBQDHn4npAgMA/icCAwDfxwIDANcXAgInbwIDAMZt' PRIVATE_DER = base64.standard_b64decode(B64PRIV_DER) -B64PUB_DER = b('MAwCBQDeKYlRAgMBAAE=') +B64PUB_DER = b'MAwCBQDeKYlRAgMBAAE=' PUBLIC_DER = base64.standard_b64decode(B64PUB_DER) -PRIVATE_PEM = b(''' +PRIVATE_PEM = b'''\ -----BEGIN CONFUSING STUFF----- Cruft before the key -----BEGIN RSA PRIVATE KEY----- Comment: something blah -%s +''' + B64PRIV_DER + b''' -----END RSA PRIVATE KEY----- Stuff after the key -----END CONFUSING STUFF----- -''' % B64PRIV_DER.decode("utf-8")) +''' -CLEAN_PRIVATE_PEM = b('''\ +CLEAN_PRIVATE_PEM = b'''\ -----BEGIN RSA PRIVATE KEY----- -%s +''' + B64PRIV_DER + b''' -----END RSA PRIVATE KEY----- -''' % B64PRIV_DER.decode("utf-8")) +''' -PUBLIC_PEM = b(''' +PUBLIC_PEM = b'''\ -----BEGIN CONFUSING STUFF----- Cruft before the key -----BEGIN RSA PUBLIC KEY----- Comment: something blah -%s +''' + B64PUB_DER + b''' -----END RSA PUBLIC KEY----- Stuff after the key -----END CONFUSING STUFF----- -''' % B64PUB_DER.decode("utf-8")) +''' -CLEAN_PUBLIC_PEM = b('''\ +CLEAN_PUBLIC_PEM = b'''\ -----BEGIN RSA PUBLIC KEY----- -%s +''' + B64PUB_DER + b''' -----END RSA PUBLIC KEY----- -''' % B64PUB_DER.decode("utf-8")) +''' class DerTest(unittest.TestCase): @@ -82,6 +80,39 @@ def test_load_private_key(self): expected = rsa.key.PrivateKey(3727264081, 65537, 3349121513, 65063, 57287) self.assertEqual(expected, key) + self.assertEqual(key.exp1, 55063) + self.assertEqual(key.exp2, 10095) + self.assertEqual(key.coef, 50797) + + @mock.patch('pyasn1.codec.der.decoder.decode') + def test_load_malformed_private_key(self, der_decode): + """Test loading malformed private DER keys.""" + + # Decode returns an invalid exp2 value. + der_decode.return_value = ( + [0, 3727264081, 65537, 3349121513, 65063, 57287, 55063, 0, 50797], + 0, + ) + + with warnings.catch_warnings(record=True) as w: + # Always print warnings + warnings.simplefilter('always') + + # Load 3 keys + for _ in range(3): + key = rsa.key.PrivateKey.load_pkcs1(PRIVATE_DER, 'DER') + + # Check that 3 warnings were generated. + self.assertEqual(3, len(w)) + + for warning in w: + self.assertTrue(issubclass(warning.category, UserWarning)) + self.assertIn('malformed', str(warning.message)) + + # Check that we are creating the key with correct values + self.assertEqual(key.exp1, 55063) + self.assertEqual(key.exp2, 10095) + self.assertEqual(key.coef, 50797) def test_save_private_key(self): """Test saving private DER keys.""" @@ -89,6 +120,7 @@ def test_save_private_key(self): key = rsa.key.PrivateKey(3727264081, 65537, 3349121513, 65063, 57287) der = key.save_pkcs1('DER') + self.assertIsInstance(der, bytes) self.assertEqual(PRIVATE_DER, der) def test_load_public_key(self): @@ -105,6 +137,7 @@ def test_save_public_key(self): key = rsa.key.PublicKey(3727264081, 65537) der = key.save_pkcs1('DER') + self.assertIsInstance(der, bytes) self.assertEqual(PUBLIC_DER, der) @@ -118,6 +151,9 @@ def test_load_private_key(self): expected = rsa.key.PrivateKey(3727264081, 65537, 3349121513, 65063, 57287) self.assertEqual(expected, key) + self.assertEqual(key.exp1, 55063) + self.assertEqual(key.exp2, 10095) + self.assertEqual(key.coef, 50797) def test_save_private_key(self): """Test saving private PEM files.""" @@ -125,6 +161,7 @@ def test_save_private_key(self): key = rsa.key.PrivateKey(3727264081, 65537, 3349121513, 65063, 57287) pem = key.save_pkcs1('PEM') + self.assertIsInstance(pem, bytes) self.assertEqual(CLEAN_PRIVATE_PEM, pem) def test_load_public_key(self): @@ -141,6 +178,7 @@ def test_save_public_key(self): key = rsa.key.PublicKey(3727264081, 65537) pem = key.save_pkcs1('PEM') + self.assertIsInstance(pem, bytes) self.assertEqual(CLEAN_PUBLIC_PEM, pem) def test_load_from_disk(self): diff --git a/tests/test_mypy.py b/tests/test_mypy.py new file mode 100644 index 0000000..c2a9745 --- /dev/null +++ b/tests/test_mypy.py @@ -0,0 +1,27 @@ +import pathlib +import unittest + +import mypy.api + +test_modules = ['rsa', 'tests'] + + +class MypyRunnerTest(unittest.TestCase): + def test_run_mypy(self): + proj_root = pathlib.Path(__file__).parent.parent + args = ['--incremental', '--ignore-missing-imports'] + [str(proj_root / dirname) for dirname + in test_modules] + + result = mypy.api.run(args) + + stdout, stderr, status = result + + messages = [] + if stderr: + messages.append(stderr) + if stdout: + messages.append(stdout) + if status: + messages.append('Mypy failed with status %d' % status) + if messages: + self.fail('\n'.join(['Mypy errors:'] + messages)) diff --git a/tests/test_pem.py b/tests/test_pem.py index 952ec79..dd03cca 100644 --- a/tests/test_pem.py +++ b/tests/test_pem.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +15,6 @@ import unittest -from rsa._compat import b from rsa.pem import _markers import rsa.key @@ -49,8 +46,8 @@ class TestMarkers(unittest.TestCase): def test_values(self): self.assertEqual(_markers('RSA PRIVATE KEY'), - (b('-----BEGIN RSA PRIVATE KEY-----'), - b('-----END RSA PRIVATE KEY-----'))) + (b'-----BEGIN RSA PRIVATE KEY-----', + b'-----END RSA PRIVATE KEY-----')) class TestBytesAndStrings(unittest.TestCase): @@ -72,3 +69,31 @@ def test_bytes_private(self): key = rsa.key.PrivateKey.load_pkcs1(private_key_pem.encode('ascii')) self.assertEqual(prime1, key.p) self.assertEqual(prime2, key.q) + + +class TestByteOutput(unittest.TestCase): + """Tests that PEM and DER are returned as bytes.""" + + def test_bytes_public(self): + key = rsa.key.PublicKey.load_pkcs1_openssl_pem(public_key_pem) + self.assertIsInstance(key.save_pkcs1(format='DER'), bytes) + self.assertIsInstance(key.save_pkcs1(format='PEM'), bytes) + + def test_bytes_private(self): + key = rsa.key.PrivateKey.load_pkcs1(private_key_pem) + self.assertIsInstance(key.save_pkcs1(format='DER'), bytes) + self.assertIsInstance(key.save_pkcs1(format='PEM'), bytes) + + +class TestByteInput(unittest.TestCase): + """Tests that PEM and DER can be loaded from bytes.""" + + def test_bytes_public(self): + key = rsa.key.PublicKey.load_pkcs1_openssl_pem(public_key_pem.encode('ascii')) + self.assertIsInstance(key.save_pkcs1(format='DER'), bytes) + self.assertIsInstance(key.save_pkcs1(format='PEM'), bytes) + + def test_bytes_private(self): + key = rsa.key.PrivateKey.load_pkcs1(private_key_pem.encode('ascii')) + self.assertIsInstance(key.save_pkcs1(format='DER'), bytes) + self.assertIsInstance(key.save_pkcs1(format='PEM'), bytes) diff --git a/tests/test_pkcs1.py b/tests/test_pkcs1.py index 39555f6..702ce2d 100644 --- a/tests/test_pkcs1.py +++ b/tests/test_pkcs1.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,7 +19,7 @@ import rsa from rsa import pkcs1 -from rsa._compat import byte, b, is_bytes +from rsa._compat import byte class BinaryTest(unittest.TestCase): @@ -46,9 +44,10 @@ def test_decoding_failure(self): # Alter the encrypted stream a = encrypted[5] - if is_bytes(a): - a = ord(a) - encrypted = encrypted[:5] + byte(a + 1) + encrypted[6:] + self.assertIsInstance(a, int) + + altered_a = (a + 1) % 256 + encrypted = encrypted[:5] + byte(altered_a) + encrypted[6:] self.assertRaises(pkcs1.DecryptionError, pkcs1.decrypt, encrypted, self.priv) @@ -65,6 +64,32 @@ def test_randomness(self): self.assertNotEqual(encrypted1, encrypted2) +class ExtraZeroesTest(unittest.TestCase): + def setUp(self): + # Key, cyphertext, and plaintext taken from https://github.com/sybrenstuvel/python-rsa/issues/146 + self.private_key = rsa.PrivateKey.load_pkcs1( + "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAs1EKK81M5kTFtZSuUFnhKy8FS2WNXaWVmi/fGHG4CLw98+Yo\n0nkuUarVwSS0O9pFPcpc3kvPKOe9Tv+6DLS3Qru21aATy2PRqjqJ4CYn71OYtSwM\n/ZfSCKvrjXybzgu+sBmobdtYm+sppbdL+GEHXGd8gdQw8DDCZSR6+dPJFAzLZTCd\nB+Ctwe/RXPF+ewVdfaOGjkZIzDoYDw7n+OHnsYCYozkbTOcWHpjVevipR+IBpGPi\n1rvKgFnlcG6d/tj0hWRl/6cS7RqhjoiNEtxqoJzpXs/Kg8xbCxXbCchkf11STA8u\ndiCjQWuWI8rcDwl69XMmHJjIQAqhKvOOQ8rYTQIDAQABAoIBABpQLQ7qbHtp4h1Y\nORAfcFRW7Q74UvtH/iEHH1TF8zyM6wZsYtcn4y0mxYE3Mp+J0xlTJbeVJkwZXYVH\nL3UH29CWHSlR+TWiazTwrCTRVJDhEoqbcTiRW8fb+o/jljVxMcVDrpyYUHNo2c6w\njBxhmKPtp66hhaDpds1Cwi0A8APZ8Z2W6kya/L/hRBzMgCz7Bon1nYBMak5PQEwV\nF0dF7Wy4vIjvCzO6DSqA415DvJDzUAUucgFudbANNXo4HJwNRnBpymYIh8mHdmNJ\n/MQ0YLSqUWvOB57dh7oWQwe3UsJ37ZUorTugvxh3NJ7Tt5ZqbCQBEECb9ND63gxo\n/a3YR/0CgYEA7BJc834xCi/0YmO5suBinWOQAF7IiRPU+3G9TdhWEkSYquupg9e6\nK9lC5k0iP+t6I69NYF7+6mvXDTmv6Z01o6oV50oXaHeAk74O3UqNCbLe9tybZ/+F\ndkYlwuGSNttMQBzjCiVy0+y0+Wm3rRnFIsAtd0RlZ24aN3bFTWJINIsCgYEAwnQq\nvNmJe9SwtnH5c/yCqPhKv1cF/4jdQZSGI6/p3KYNxlQzkHZ/6uvrU5V27ov6YbX8\nvKlKfO91oJFQxUD6lpTdgAStI3GMiJBJIZNpyZ9EWNSvwUj28H34cySpbZz3s4Xd\nhiJBShgy+fKURvBQwtWmQHZJ3EGrcOI7PcwiyYcCgYEAlql5jSUCY0ALtidzQogW\nJ+B87N+RGHsBuJ/0cxQYinwg+ySAAVbSyF1WZujfbO/5+YBN362A/1dn3lbswCnH\nK/bHF9+fZNqvwprPnceQj5oK1n4g6JSZNsy6GNAhosT+uwQ0misgR8SQE4W25dDG\nkdEYsz+BgCsyrCcu8J5C+tUCgYAFVPQbC4f2ikVyKzvgz0qx4WUDTBqRACq48p6e\n+eLatv7nskVbr7QgN+nS9+Uz80ihR0Ev1yCAvnwmM/XYAskcOea87OPmdeWZlQM8\nVXNwINrZ6LMNBLgorfuTBK1UoRo1pPUHCYdqxbEYI2unak18mikd2WB7Fp3h0YI4\nVpGZnwKBgBxkAYnZv+jGI4MyEKdsQgxvROXXYOJZkWzsKuKxVkVpYP2V4nR2YMOJ\nViJQ8FUEnPq35cMDlUk4SnoqrrHIJNOvcJSCqM+bWHAioAsfByLbUPM8sm3CDdIk\nXVJl32HuKYPJOMIWfc7hIfxLRHnCN+coz2M6tgqMDs0E/OfjuqVZ\n-----END RSA PRIVATE KEY-----", + format='PEM') + self.cyphertext = bytes.fromhex( + "4501b4d669e01b9ef2dc800aa1b06d49196f5a09fe8fbcd037323c60eaf027bfb98432be4e4a26c567ffec718bcbea977dd26812fa071c33808b4d5ebb742d9879806094b6fbeea63d25ea3141733b60e31c6912106e1b758a7fe0014f075193faa8b4622bfd5d3013f0a32190a95de61a3604711bc62945f95a6522bd4dfed0a994ef185b28c281f7b5e4c8ed41176d12d9fc1b837e6a0111d0132d08a6d6f0580de0c9eed8ed105531799482d1e466c68c23b0c222af7fc12ac279bc4ff57e7b4586d209371b38c4c1035edd418dc5f960441cb21ea2bedbfea86de0d7861e81021b650a1de51002c315f1e7c12debe4dcebf790caaa54a2f26b149cf9e77d" + ) + self.plaintext = bytes.fromhex("54657374") + + def test_unmodified(self): + message = rsa.decrypt(self.cyphertext, self.private_key) + self.assertEqual(message, self.plaintext) + + def test_prepend_zeroes(self): + cyphertext = bytes.fromhex("0000") + self.cyphertext + with self.assertRaises(rsa.DecryptionError): + rsa.decrypt(cyphertext, self.private_key) + + def test_append_zeroes(self): + cyphertext = self.cyphertext + bytes.fromhex("0000") + with self.assertRaises(rsa.DecryptionError): + rsa.decrypt(cyphertext, self.private_key) + + class SignatureTest(unittest.TestCase): def setUp(self): (self.pub, self.priv) = rsa.newkeys(512) @@ -72,27 +97,34 @@ def setUp(self): def test_sign_verify(self): """Test happy flow of sign and verify""" - message = b('je moeder') - print("\tMessage: %r" % message) + message = b'je moeder' + signature = pkcs1.sign(message, self.priv, 'SHA-256') + self.assertEqual('SHA-256', pkcs1.verify(message, signature, self.pub)) + signature = pkcs1.sign(message, self.priv, 'SHA3-256') + self.assertEqual('SHA3-256', pkcs1.verify(message, signature, self.pub)) + + def test_find_signature_hash(self): + """Test happy flow of sign and find_signature_hash""" + + message = b'je moeder' signature = pkcs1.sign(message, self.priv, 'SHA-256') - print("\tSignature: %r" % signature) - self.assertTrue(pkcs1.verify(message, signature, self.pub)) + self.assertEqual('SHA-256', pkcs1.find_signature_hash(signature, self.pub)) def test_alter_message(self): """Altering the message should let the verification fail.""" - signature = pkcs1.sign(b('je moeder'), self.priv, 'SHA-256') + signature = pkcs1.sign(b'je moeder', self.priv, 'SHA-256') self.assertRaises(pkcs1.VerificationError, pkcs1.verify, - b('mijn moeder'), signature, self.pub) + b'mijn moeder', signature, self.pub) def test_sign_different_key(self): """Signing with another key should let the verification fail.""" (otherpub, _) = rsa.newkeys(512) - message = b('je moeder') + message = b'je moeder' signature = pkcs1.sign(message, self.priv, 'SHA-256') self.assertRaises(pkcs1.VerificationError, pkcs1.verify, message, signature, otherpub) @@ -105,3 +137,42 @@ def test_multiple_signings(self): signature2 = pkcs1.sign(message, self.priv, 'SHA-1') self.assertEqual(signature1, signature2) + + def test_split_hash_sign(self): + """Hashing and then signing should match with directly signing the message. """ + + message = b'je moeder' + msg_hash = pkcs1.compute_hash(message, 'SHA-256') + signature1 = pkcs1.sign_hash(msg_hash, self.priv, 'SHA-256') + + # Calculate the signature using the unified method + signature2 = pkcs1.sign(message, self.priv, 'SHA-256') + + self.assertEqual(signature1, signature2) + + def test_hash_sign_verify(self): + """Test happy flow of hash, sign, and verify""" + + message = b'je moeder' + msg_hash = pkcs1.compute_hash(message, 'SHA-224') + signature = pkcs1.sign_hash(msg_hash, self.priv, 'SHA-224') + + self.assertTrue(pkcs1.verify(message, signature, self.pub)) + + def test_prepend_zeroes(self): + """Prepending the signature with zeroes should be detected.""" + + message = b'je moeder' + signature = pkcs1.sign(message, self.priv, 'SHA-256') + signature = bytes.fromhex('0000') + signature + with self.assertRaises(rsa.VerificationError): + pkcs1.verify(message, signature, self.pub) + + def test_apppend_zeroes(self): + """Apppending the signature with zeroes should be detected.""" + + message = b'je moeder' + signature = pkcs1.sign(message, self.priv, 'SHA-256') + signature = signature + bytes.fromhex('0000') + with self.assertRaises(rsa.VerificationError): + pkcs1.verify(message, signature, self.pub) diff --git a/tests/test_pkcs1_v2.py b/tests/test_pkcs1_v2.py new file mode 100644 index 0000000..bba525e --- /dev/null +++ b/tests/test_pkcs1_v2.py @@ -0,0 +1,81 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests PKCS #1 version 2 functionality. + +Most of the mocked values come from the test vectors found at: +http://www.itomorrowmag.com/emc-plus/rsa-labs/standards-initiatives/pkcs-rsa-cryptography-standard.htm +""" + +import unittest + +from rsa import pkcs1_v2 + + +class MGFTest(unittest.TestCase): + def test_oaep_int_db_mask(self): + seed = ( + b'\xaa\xfd\x12\xf6\x59\xca\xe6\x34\x89\xb4\x79\xe5\x07\x6d\xde\xc2' + b'\xf0\x6c\xb5\x8f' + ) + db = ( + b'\xda\x39\xa3\xee\x5e\x6b\x4b\x0d\x32\x55\xbf\xef\x95\x60\x18\x90' + b'\xaf\xd8\x07\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xd4\x36\xe9\x95\x69' + b'\xfd\x32\xa7\xc8\xa0\x5b\xbc\x90\xd3\x2c\x49' + ) + masked_db = ( + b'\xdc\xd8\x7d\x5c\x68\xf1\xee\xa8\xf5\x52\x67\xc3\x1b\x2e\x8b\xb4' + b'\x25\x1f\x84\xd7\xe0\xb2\xc0\x46\x26\xf5\xaf\xf9\x3e\xdc\xfb\x25' + b'\xc9\xc2\xb3\xff\x8a\xe1\x0e\x83\x9a\x2d\xdb\x4c\xdc\xfe\x4f\xf4' + b'\x77\x28\xb4\xa1\xb7\xc1\x36\x2b\xaa\xd2\x9a\xb4\x8d\x28\x69\xd5' + b'\x02\x41\x21\x43\x58\x11\x59\x1b\xe3\x92\xf9\x82\xfb\x3e\x87\xd0' + b'\x95\xae\xb4\x04\x48\xdb\x97\x2f\x3a\xc1\x4f\x7b\xc2\x75\x19\x52' + b'\x81\xce\x32\xd2\xf1\xb7\x6d\x4d\x35\x3e\x2d' + ) + + # dbMask = MGF(seed, length(DB)) + db_mask = pkcs1_v2.mgf1(seed, length=len(db)) + expected_db_mask = ( + b'\x06\xe1\xde\xb2\x36\x9a\xa5\xa5\xc7\x07\xd8\x2c\x8e\x4e\x93\x24' + b'\x8a\xc7\x83\xde\xe0\xb2\xc0\x46\x26\xf5\xaf\xf9\x3e\xdc\xfb\x25' + b'\xc9\xc2\xb3\xff\x8a\xe1\x0e\x83\x9a\x2d\xdb\x4c\xdc\xfe\x4f\xf4' + b'\x77\x28\xb4\xa1\xb7\xc1\x36\x2b\xaa\xd2\x9a\xb4\x8d\x28\x69\xd5' + b'\x02\x41\x21\x43\x58\x11\x59\x1b\xe3\x92\xf9\x82\xfb\x3e\x87\xd0' + b'\x95\xae\xb4\x04\x48\xdb\x97\x2f\x3a\xc1\x4e\xaf\xf4\x9c\x8c\x3b' + b'\x7c\xfc\x95\x1a\x51\xec\xd1\xdd\xe6\x12\x64' + ) + + self.assertEqual(db_mask, expected_db_mask) + + # seedMask = MGF(maskedDB, length(seed)) + seed_mask = pkcs1_v2.mgf1(masked_db, length=len(seed)) + expected_seed_mask = ( + b'\x41\x87\x0b\x5a\xb0\x29\xe6\x57\xd9\x57\x50\xb5\x4c\x28\x3c\x08' + b'\x72\x5d\xbe\xa9' + ) + + self.assertEqual(seed_mask, expected_seed_mask) + + def test_invalid_hasher(self): + """Tests an invalid hasher generates an exception""" + with self.assertRaises(ValueError): + pkcs1_v2.mgf1(b'\x06\xe1\xde\xb2', length=8, hasher='SHA2') + + def test_invalid_length(self): + with self.assertRaises(OverflowError): + pkcs1_v2.mgf1(b'\x06\xe1\xde\xb2', length=2**50) diff --git a/tests/test_prime.py b/tests/test_prime.py index a47c3f2..5577f67 100644 --- a/tests/test_prime.py +++ b/tests/test_prime.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +17,7 @@ import unittest import rsa.prime +import rsa.randnum class PrimeTest(unittest.TestCase): @@ -42,3 +41,67 @@ def test_is_prime(self): # Test around the 50th millionth known prime. self.assertTrue(rsa.prime.is_prime(982451653)) self.assertFalse(rsa.prime.is_prime(982451653 * 961748941)) + + def test_miller_rabin_primality_testing(self): + """Uses monkeypatching to ensure certain random numbers. + + This allows us to predict/control the code path. + """ + + randints = [] + + def fake_randint(maxvalue): + return randints.pop(0) + + orig_randint = rsa.randnum.randint + rsa.randnum.randint = fake_randint + try: + # 'n is composite' + randints.append(2630484832) # causes the 'n is composite' case with n=3784949785 + self.assertEqual(False, rsa.prime.miller_rabin_primality_testing(2787998641, 7)) + self.assertEqual([], randints) + + # 'Exit inner loop and continue with next witness' + randints.extend([ + 2119139098, # causes 'Exit inner loop and continue with next witness' + # the next witnesses for the above case: + 3051067716, 3603501763, 3230895847, 3687808133, 3760099987, 4026931495, 3022471882, + ]) + self.assertEqual(True, rsa.prime.miller_rabin_primality_testing(2211417913, + len(randints))) + self.assertEqual([], randints) + finally: + rsa.randnum.randint = orig_randint + + def test_mersenne_primes(self): + """Tests first known Mersenne primes. + + Mersenne primes are prime numbers that can be written in the form + `Mn = 2**n - 1` for some integer `n`. For the list of known Mersenne + primes, see: + https://en.wikipedia.org/wiki/Mersenne_prime#List_of_known_Mersenne_primes + """ + + # List of known Mersenne exponents. + known_mersenne_exponents = [ + 2, 3, 5, 7, 13, 17, 19, 31, 61, 89, 107, 127, 521, 607, 1279, + 2203, 2281, 4423, + ] + + # Test Mersenne primes. + for exp in known_mersenne_exponents: + self.assertTrue(rsa.prime.is_prime(2**exp - 1)) + + def test_get_primality_testing_rounds(self): + """Test round calculation for primality testing.""" + + self.assertEqual(rsa.prime.get_primality_testing_rounds(1 << 63), 10) + self.assertEqual(rsa.prime.get_primality_testing_rounds(1 << 127), 10) + self.assertEqual(rsa.prime.get_primality_testing_rounds(1 << 255), 10) + self.assertEqual(rsa.prime.get_primality_testing_rounds(1 << 511), 7) + self.assertEqual(rsa.prime.get_primality_testing_rounds(1 << 767), 7) + self.assertEqual(rsa.prime.get_primality_testing_rounds(1 << 1023), 4) + self.assertEqual(rsa.prime.get_primality_testing_rounds(1 << 1279), 4) + self.assertEqual(rsa.prime.get_primality_testing_rounds(1 << 1535), 3) + self.assertEqual(rsa.prime.get_primality_testing_rounds(1 << 2047), 3) + self.assertEqual(rsa.prime.get_primality_testing_rounds(1 << 4095), 3) diff --git a/tests/test_strings.py b/tests/test_strings.py index 28fa091..1090a8e 100644 --- a/tests/test_strings.py +++ b/tests/test_strings.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,12 +29,12 @@ def setUp(self): def test_enc_dec(self): message = unicode_string.encode('utf-8') - print("\tMessage: %s" % message) + print("\tMessage: %r" % message) encrypted = rsa.encrypt(message, self.pub) - print("\tEncrypted: %s" % encrypted) + print("\tEncrypted: %r" % encrypted) decrypted = rsa.decrypt(encrypted, self.priv) - print("\tDecrypted: %s" % decrypted) + print("\tDecrypted: %r" % decrypted) self.assertEqual(message, decrypted) diff --git a/tests/test_transform.py b/tests/test_transform.py index 7fe121b..7b9335e 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2011 Sybren A. Stüvel # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,37 +13,25 @@ # limitations under the License. import unittest -from rsa._compat import b -from rsa.transform import int2bytes, bytes2int, _int2bytes +from rsa.transform import int2bytes, bytes2int class Test_int2bytes(unittest.TestCase): def test_accuracy(self): - self.assertEqual(int2bytes(123456789), b('\x07[\xcd\x15')) - self.assertEqual(_int2bytes(123456789), b('\x07[\xcd\x15')) + self.assertEqual(int2bytes(123456789), b'\x07[\xcd\x15') def test_codec_identity(self): self.assertEqual(bytes2int(int2bytes(123456789, 128)), 123456789) - self.assertEqual(bytes2int(_int2bytes(123456789, 128)), 123456789) def test_chunk_size(self): - self.assertEqual(int2bytes(123456789, 6), b('\x00\x00\x07[\xcd\x15')) + self.assertEqual(int2bytes(123456789, 6), b'\x00\x00\x07[\xcd\x15') self.assertEqual(int2bytes(123456789, 7), - b('\x00\x00\x00\x07[\xcd\x15')) - - self.assertEqual(_int2bytes(123456789, 6), - b('\x00\x00\x07[\xcd\x15')) - self.assertEqual(_int2bytes(123456789, 7), - b('\x00\x00\x00\x07[\xcd\x15')) + b'\x00\x00\x00\x07[\xcd\x15') def test_zero(self): - self.assertEqual(int2bytes(0, 4), b('\x00') * 4) - self.assertEqual(int2bytes(0, 7), b('\x00') * 7) - self.assertEqual(int2bytes(0), b('\x00')) - - self.assertEqual(_int2bytes(0, 4), b('\x00') * 4) - self.assertEqual(_int2bytes(0, 7), b('\x00') * 7) - self.assertEqual(_int2bytes(0), b('\x00')) + self.assertEqual(int2bytes(0, 4), b'\x00' * 4) + self.assertEqual(int2bytes(0, 7), b'\x00' * 7) + self.assertEqual(int2bytes(0), b'\x00') def test_correctness_against_base_implementation(self): # Slow test. @@ -55,26 +41,16 @@ def test_correctness_against_base_implementation(self): 1 << 77, ] for value in values: - self.assertEqual(int2bytes(value), _int2bytes(value), - "Boom %d" % value) self.assertEqual(bytes2int(int2bytes(value)), value, "Boom %d" % value) - self.assertEqual(bytes2int(_int2bytes(value)), - value, - "Boom %d" % value) def test_raises_OverflowError_when_chunk_size_is_insufficient(self): self.assertRaises(OverflowError, int2bytes, 123456789, 3) self.assertRaises(OverflowError, int2bytes, 299999999999, 4) - self.assertRaises(OverflowError, _int2bytes, 123456789, 3) - self.assertRaises(OverflowError, _int2bytes, 299999999999, 4) - def test_raises_ValueError_when_negative_integer(self): self.assertRaises(ValueError, int2bytes, -1) - self.assertRaises(ValueError, _int2bytes, -1) def test_raises_TypeError_when_not_integer(self): self.assertRaises(TypeError, int2bytes, None) - self.assertRaises(TypeError, _int2bytes, None) diff --git a/tests/test_varblock.py b/tests/test_varblock.py deleted file mode 100644 index d1c3730..0000000 --- a/tests/test_varblock.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2011 Sybren A. Stüvel -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests varblock operations.""" - -try: - from StringIO import StringIO as BytesIO -except ImportError: - from io import BytesIO -import unittest - -from rsa._compat import b -from rsa import varblock - - -class VarintTest(unittest.TestCase): - def test_read_varint(self): - encoded = b('\xac\x02crummy') - infile = BytesIO(encoded) - - (decoded, read) = varblock.read_varint(infile) - - # Test the returned values - self.assertEqual(300, decoded) - self.assertEqual(2, read) - - # The rest of the file should be untouched - self.assertEqual(b('crummy'), infile.read()) - - def test_read_zero(self): - encoded = b('\x00crummy') - infile = BytesIO(encoded) - - (decoded, read) = varblock.read_varint(infile) - - # Test the returned values - self.assertEqual(0, decoded) - self.assertEqual(1, read) - - # The rest of the file should be untouched - self.assertEqual(b('crummy'), infile.read()) - - def test_write_varint(self): - expected = b('\xac\x02') - outfile = BytesIO() - - written = varblock.write_varint(outfile, 300) - - # Test the returned values - self.assertEqual(expected, outfile.getvalue()) - self.assertEqual(2, written) - - def test_write_zero(self): - outfile = BytesIO() - written = varblock.write_varint(outfile, 0) - - # Test the returned values - self.assertEqual(b('\x00'), outfile.getvalue()) - self.assertEqual(1, written) - - -class VarblockTest(unittest.TestCase): - def test_yield_varblock(self): - infile = BytesIO(b('\x01\x0512345\x06Sybren')) - - varblocks = list(varblock.yield_varblocks(infile)) - self.assertEqual([b('12345'), b('Sybren')], varblocks) - - -class FixedblockTest(unittest.TestCase): - def test_yield_fixedblock(self): - infile = BytesIO(b('123456Sybren')) - - fixedblocks = list(varblock.yield_fixedblocks(infile, 6)) - self.assertEqual([b('123456'), b('Sybren')], fixedblocks) diff --git a/tox.ini b/tox.ini index 2aa7823..0446ef5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,19 @@ [tox] # Environment changes have to be manually synced with '.travis.yml'. -envlist = py26,py27,py33,py34,py35,pypy +envlist = py35,py36,p37,p38 +isolated_build = true [pytest] addopts = -v --cov rsa --cov-report term-missing [testenv] -commands=py.test [] -deps=pyasn1 >=0.1.3 - coverage >=3.5 - PyTest - pytest-xdist - pytest-cov +whitelist_externals = poetry +commands = + poetry install -v + poetry run py.test tests/ -[testenv:py35] -commands=py.test --doctest-modules [] - -[pep8] -max-line-length = 100 +[testenv:py37] +whitelist_externals = poetry +commands= + poetry install -v + poetry run py.test --doctest-modules rsa tests/ diff --git a/update_version.sh b/update_version.sh new file mode 100755 index 0000000..313b7a5 --- /dev/null +++ b/update_version.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +if [ -z "$1" ]; then + echo "Usage: $0 new-version" >&2 + exit 1 +fi + +DATE=$(date +'%Y-%m-%d') + +sed "s/__date__\s=\s'[^']*'/__date__ = '$DATE'/" -i rsa/__init__.py +sed "s/__version__\s=\s'[^']*'/__version__ = '$1'/" -i rsa/__init__.py + +poetry version $1 + +git diff +echo +echo "Don't forget to commit and tag:" +echo git commit -m \'Bumped version to $1\' pyproject.toml rsa/__init__.py +echo git tag -a version-$1 -m \'Tagged version $1\'