diff options
-rw-r--r-- | AUTHORS.rst | 1 | ||||
-rw-r--r-- | CHANGELOG.rst | 35 | ||||
-rw-r--r-- | MANIFEST.in | 5 | ||||
-rw-r--r-- | cryptography/hazmat/bindings/openssl/ssl.py | 6 | ||||
-rw-r--r-- | cryptography/hazmat/primitives/asymmetric/rsa.py | 2 | ||||
-rw-r--r-- | cryptography/hazmat/primitives/twofactor/hotp.py | 22 | ||||
-rw-r--r-- | cryptography/hazmat/primitives/twofactor/totp.py | 32 | ||||
-rw-r--r-- | docs/changelog.rst | 35 | ||||
-rw-r--r-- | docs/development/submitting-patches.rst | 5 | ||||
-rw-r--r-- | docs/hazmat/primitives/symmetric-encryption.rst | 88 | ||||
-rw-r--r-- | docs/hazmat/primitives/twofactor.rst | 138 | ||||
-rw-r--r-- | docs/random-numbers.rst | 6 | ||||
-rw-r--r-- | setup.py | 28 | ||||
-rw-r--r-- | tests/hazmat/primitives/twofactor/test_hotp.py | 22 | ||||
-rw-r--r-- | tests/hazmat/primitives/twofactor/test_totp.py | 129 | ||||
-rw-r--r-- | tox.ini | 1 |
16 files changed, 406 insertions, 149 deletions
diff --git a/AUTHORS.rst b/AUTHORS.rst index e9c2f85f..c06faf1a 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -15,3 +15,4 @@ PGP key fingerprints are enclosed in parentheses. * Konstantinos Koukopoulos <koukopoulos@gmail.com> (D6BD 52B6 8C99 A91C E2C8 934D 3300 566B 3A46 726E) * Stephen Holsapple <sholsapp@gmail.com> * Terry Chia <terrycwk1994@gmail.com> +* Matthew Iversen <matt@notevencode.com> (2F04 3DCC D6E6 D5AC D262 2E0B C046 E8A8 7452 2973) diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 00000000..bbbcfb1f --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,35 @@ +Changelog +========= + +0.3 - 2014-XX-XX +~~~~~~~~~~~~~~~~ + +* Added :class:`~cryptography.hazmat.primitives.twofactor.hotp.HOTP`. +* Added :class:`~cryptography.hazmat.primitives.twofactor.totp.TOTP`. + +0.2.1 - 2014-02-22 +~~~~~~~~~~~~~~~~~~ +* Fix a bug where importing cryptography from multiple paths could cause initialization to fail. + +0.2 - 2014-02-20 +~~~~~~~~~~~~~~~~ + +* Added :doc:`/hazmat/backends/commoncrypto`. +* Added initial :doc:`/hazmat/bindings/commoncrypto`. +* Removed ``register_cipher_adapter`` method from + :class:`~cryptography.hazmat.backends.interfaces.CipherBackend`. +* Added support for the OpenSSL backend under Windows. +* Improved thread-safety for the OpenSSL backend. +* Fixed compilation on systems where OpenSSL's ``ec.h`` header is not + available, such as CentOS. +* Added :class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC`. +* Added :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF`. +* Added :doc:`/hazmat/backends/multibackend`. +* Set default random for the :doc:`/hazmat/backends/openssl` to the OS random engine. +* Added :class:`~cryptography.hazmat.primitives.ciphers.algorithms.CAST5` (CAST-128) support. + +0.1 - 2014-01-08 +~~~~~~~~~~~~~~~~ + +* Initial release. + diff --git a/MANIFEST.in b/MANIFEST.in index c5ca0359..a252cc8d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,11 @@ -include LICENSE include AUTHORS.rst +include CHANGELOG.rst include CONTRIBUTING.rst +include LICENSE include README.rst +recursive-include docs * +prune docs/_build recursive-include tests *.py recursive-include tests/vectors * recursive-include tests/hazmat/primitives/vectors * diff --git a/cryptography/hazmat/bindings/openssl/ssl.py b/cryptography/hazmat/bindings/openssl/ssl.py index ca3e96c8..25e49672 100644 --- a/cryptography/hazmat/bindings/openssl/ssl.py +++ b/cryptography/hazmat/bindings/openssl/ssl.py @@ -136,6 +136,7 @@ typedef struct { typedef struct { SSL3_STATE *s3; SSL_SESSION *session; + int type; ...; } SSL; @@ -219,6 +220,9 @@ void SSL_SESSION_free(SSL_SESSION *); const char *SSL_CIPHER_get_name(const SSL_CIPHER *); int SSL_CIPHER_get_bits(const SSL_CIPHER *, int *); char *SSL_CIPHER_get_version(const SSL_CIPHER *); + +size_t SSL_get_finished(const SSL *, void *, size_t); +size_t SSL_get_peer_finished(const SSL *, void *, size_t); """ MACROS = """ @@ -298,6 +302,8 @@ void SSL_set_tlsext_host_name(SSL *, char *); void SSL_CTX_set_tlsext_servername_callback( SSL_CTX *, int (*)(const SSL *, int *, void *)); + +long SSL_session_reused(SSL *); """ CUSTOMIZATIONS = """ diff --git a/cryptography/hazmat/primitives/asymmetric/rsa.py b/cryptography/hazmat/primitives/asymmetric/rsa.py index a63d4308..2f9e4247 100644 --- a/cryptography/hazmat/primitives/asymmetric/rsa.py +++ b/cryptography/hazmat/primitives/asymmetric/rsa.py @@ -132,7 +132,7 @@ class RSAPrivateKey(object): self._modulus = modulus @classmethod - def generate(self, public_exponent, key_size, backend): + def generate(cls, public_exponent, key_size, backend): return backend.generate_rsa_private_key(public_exponent, key_size) def signer(self, padding, algorithm, backend): diff --git a/cryptography/hazmat/primitives/twofactor/hotp.py b/cryptography/hazmat/primitives/twofactor/hotp.py index 535a94c7..24f5f465 100644 --- a/cryptography/hazmat/primitives/twofactor/hotp.py +++ b/cryptography/hazmat/primitives/twofactor/hotp.py @@ -17,27 +17,31 @@ import struct import six -from cryptography.exceptions import InvalidToken +from cryptography.exceptions import InvalidToken, UnsupportedAlgorithm from cryptography.hazmat.primitives import constant_time, hmac -from cryptography.hazmat.primitives.hashes import SHA1 +from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SHA512 class HOTP(object): - def __init__(self, key, length, backend): - + def __init__(self, key, length, algorithm, backend): if len(key) < 16: raise ValueError("Key length has to be at least 128 bits.") if length < 6 or length > 8: raise ValueError("Length of HOTP has to be between 6 to 8.") + if not isinstance(algorithm, (SHA1, SHA256, SHA512)): + raise UnsupportedAlgorithm( + "Algorithm must be SHA1, SHA256 or SHA512") + self._key = key self._length = length + self._algorithm = algorithm self._backend = backend def generate(self, counter): truncated_value = self._dynamic_truncate(counter) - hotp = truncated_value % (10**self._length) + hotp = truncated_value % (10 ** self._length) return "{0:0{1}}".format(hotp, self._length).encode() def verify(self, hotp, counter): @@ -45,12 +49,12 @@ class HOTP(object): raise InvalidToken("Supplied HOTP value does not match") def _dynamic_truncate(self, counter): - ctx = hmac.HMAC(self._key, SHA1(), self._backend) + ctx = hmac.HMAC(self._key, self._algorithm, self._backend) ctx.update(struct.pack(">Q", counter)) hmac_value = ctx.finalize() - offset_bits = six.indexbytes(hmac_value, 19) & 0b1111 + offset_bits = six.indexbytes(hmac_value, len(hmac_value) - 1) & 0b1111 offset = int(offset_bits) - P = hmac_value[offset:offset+4] - return struct.unpack(">I", P)[0] & 0x7fffffff + p = hmac_value[offset:offset + 4] + return struct.unpack(">I", p)[0] & 0x7fffffff diff --git a/cryptography/hazmat/primitives/twofactor/totp.py b/cryptography/hazmat/primitives/twofactor/totp.py new file mode 100644 index 00000000..0630de69 --- /dev/null +++ b/cryptography/hazmat/primitives/twofactor/totp.py @@ -0,0 +1,32 @@ +# 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 +# +# http://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 __future__ import absolute_import, division, print_function + +from cryptography.exceptions import InvalidToken +from cryptography.hazmat.primitives import constant_time +from cryptography.hazmat.primitives.twofactor.hotp import HOTP + + +class TOTP(object): + def __init__(self, key, length, algorithm, time_step, backend): + self._time_step = time_step + self._hotp = HOTP(key, length, algorithm, backend) + + def generate(self, time): + counter = int(time / self._time_step) + return self._hotp.generate(counter) + + def verify(self, totp, time): + if not constant_time.bytes_eq(self.generate(time), totp): + raise InvalidToken("Supplied TOTP value does not match") diff --git a/docs/changelog.rst b/docs/changelog.rst index b87b8722..565b0521 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,34 +1 @@ -Changelog -========= - -0.3 - 2014-XX-XX -~~~~~~~~~~~~~~~~ - -* Added :class:`~cryptography.hazmat.primitives.twofactor.hotp.HOTP`. - -0.2.1 - 2014-02-22 -~~~~~~~~~~~~~~~~~~ -* Fix a bug where importing cryptography from multiple paths could cause initialization to fail. - -0.2 - 2014-02-20 -~~~~~~~~~~~~~~~~ - -* Added :doc:`/hazmat/backends/commoncrypto`. -* Added initial :doc:`/hazmat/bindings/commoncrypto`. -* Removed ``register_cipher_adapter`` method from - :class:`~cryptography.hazmat.backends.interfaces.CipherBackend`. -* Added support for the OpenSSL backend under Windows. -* Improved thread-safety for the OpenSSL backend. -* Fixed compilation on systems where OpenSSL's ``ec.h`` header is not - available, such as CentOS. -* Added :class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC`. -* Added :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF`. -* Added :doc:`/hazmat/backends/multibackend`. -* Set default random for the :doc:`/hazmat/backends/openssl` to the OS random engine. -* Added :class:`~cryptography.hazmat.primitives.ciphers.algorithms.CAST5` (CAST-128) support. - -0.1 - 2014-01-08 -~~~~~~~~~~~~~~~~ - -* Initial release. - +.. include:: ../CHANGELOG.rst diff --git a/docs/development/submitting-patches.rst b/docs/development/submitting-patches.rst index 5dca3f79..1797b9c1 100644 --- a/docs/development/submitting-patches.rst +++ b/docs/development/submitting-patches.rst @@ -15,7 +15,10 @@ follow the directions on the :doc:`security page </security>`. Code ---- -When in doubt, refer to :pep:`8` for Python code. +When in doubt, refer to :pep:`8` for Python code. You can check if your code +meets our automated requirements by running ``flake8`` against it. If you've +installed the development requirements this will automatically use our +configuration. You can also run the ``tox`` job with ``tox -e pep8``. `Write comments as complete sentences.`_ diff --git a/docs/hazmat/primitives/symmetric-encryption.rst b/docs/hazmat/primitives/symmetric-encryption.rst index d91dde9d..2306c5b7 100644 --- a/docs/hazmat/primitives/symmetric-encryption.rst +++ b/docs/hazmat/primitives/symmetric-encryption.rst @@ -88,7 +88,7 @@ Algorithms choice for encryption. :param bytes key: The secret key, either ``128``, ``192``, or ``256`` bits. - This must be kept secret. + This must be kept secret. .. class:: Camellia(key) @@ -97,7 +97,7 @@ Algorithms is not as widely studied or deployed. :param bytes key: The secret key, either ``128``, ``192``, or ``256`` bits. - This must be kept secret. + This must be kept secret. .. class:: TripleDES(key) @@ -108,12 +108,11 @@ Algorithms is incredibly slow; old applications should consider moving away from it. :param bytes key: The secret key, either ``64``, ``128``, or ``192`` bits - (note that DES functionally uses ``56``, ``112``, or - ``168`` bits of the key, there is a parity byte in each - component of the key), in some materials these are - referred to as being up to three separate keys (each - ``56`` bits long), they can simply be concatenated to - produce the full key. This must be kept secret. + (note that DES functionally uses ``56``, ``112``, or ``168`` bits of + the key, there is a parity byte in each component of the key), in some + materials these are referred to as being up to three separate keys + (each ``56`` bits long), they can simply be concatenated to produce the + full key. This must be kept secret. .. class:: CAST5(key) @@ -124,7 +123,7 @@ Algorithms a variable key length cipher and supports keys from 40-128 bits in length. :param bytes key: The secret key, 40-128 bits in length (in increments of - 8). This must be kept secret. + 8). This must be kept secret. Weak Ciphers ------------ @@ -142,7 +141,7 @@ Weak Ciphers that users of Blowfish move to newer algorithms, such as :class:`AES`. :param bytes key: The secret key, 32-448 bits in length (in increments of - 8). This must be kept secret. + 8). This must be kept secret. .. class:: ARC4(key) @@ -151,8 +150,7 @@ Weak Ciphers mode constructions. :param bytes key: The secret key, ``40``, ``56``, ``64``, ``80``, ``128``, - ``192``, or ``256`` bits in length. This must be kept - secret. + ``192``, or ``256`` bits in length. This must be kept secret. .. doctest:: @@ -182,17 +180,12 @@ Modes **Padding is required when using this mode.** :param bytes initialization_vector: Must be random bytes. They do not need - to be kept secret (they can be included - in a transmitted message). Must be the - same number of bytes as the - ``block_size`` of the cipher. Each time - something is encrypted a new - ``initialization_vector`` should be - generated. Do not reuse an - ``initialization_vector`` with - a given ``key``, and particularly do - not use a constant - ``initialization_vector``. + to be kept secret (they can be included in a transmitted message). Must + be the same number of bytes as the ``block_size`` of the cipher. Each + time something is encrypted a new ``initialization_vector`` should be + generated. Do not reuse an ``initialization_vector`` with a given + ``key``, and particularly do not use a constant + ``initialization_vector``. A good construction looks like: @@ -226,12 +219,11 @@ Modes **This mode does not require padding.** :param bytes nonce: Should be random bytes. It is critical to never reuse a - ``nonce`` with a given key. Any reuse of a nonce - with the same key compromises the security of every - message encrypted with that key. Must be the same - number of bytes as the ``block_size`` of the cipher - with a given key. The nonce does not need to be kept - secret and may be included alongside the ciphertext. + ``nonce`` with a given key. Any reuse of a nonce with the same key + compromises the security of every message encrypted with that key. Must + be the same number of bytes as the ``block_size`` of the cipher with a + given key. The nonce does not need to be kept secret and may be + included alongside the ciphertext. .. class:: OFB(initialization_vector) @@ -241,12 +233,9 @@ Modes **This mode does not require padding.** :param bytes initialization_vector: Must be random bytes. They do not need - to be kept secret (they can be included - in a transmitted message). Must be the - same number of bytes as the - ``block_size`` of the cipher. Do not - reuse an ``initialization_vector`` with - a given ``key``. + to be kept secret (they can be included in a transmitted message). Must + be the same number of bytes as the ``block_size`` of the cipher. Do not + reuse an ``initialization_vector`` with a given ``key``. .. class:: CFB(initialization_vector) @@ -256,12 +245,9 @@ Modes **This mode does not require padding.** :param bytes initialization_vector: Must be random bytes. They do not need - to be kept secret (they can be included - in a transmitted message). Must be the - same number of bytes as the - ``block_size`` of the cipher. Do not - reuse an ``initialization_vector`` with - a given ``key``. + to be kept secret (they can be included in a transmitted message). Must + be the same number of bytes as the ``block_size`` of the cipher. Do not + reuse an ``initialization_vector`` with a given ``key``. .. class:: GCM(initialization_vector, tag=None) @@ -282,13 +268,10 @@ Modes **This mode does not require padding.** :param bytes initialization_vector: Must be random bytes. They do not need - to be kept secret (they can be included - in a transmitted message). NIST - `recommends 96-bit IV length`_ for - performance critical situations, but it - can be up to 2\ :sup:`64` - 1 bits. - Do not reuse an ``initialization_vector`` - with a given ``key``. + to be kept secret (they can be included in a transmitted message). NIST + `recommends 96-bit IV length`_ for performance critical situations, but + it can be up to 2\ :sup:`64` - 1 bits. Do not reuse an + ``initialization_vector`` with a given ``key``. .. note:: @@ -300,8 +283,8 @@ Modes (32-bits). Applications **must** verify the tag is the expected length to guarantee the expected security margin. - :param bytes tag: The tag bytes to verify during decryption. When encrypting - this must be None. + :param bytes tag: The tag bytes to verify during decryption. When + encrypting this must be ``None``. .. testcode:: @@ -428,8 +411,7 @@ Interfaces :return bytes: Returns the remainder of the data. :raises ValueError: This is raised when the data provided isn't - correctly padded to be a multiple of the - algorithm's block size. + correctly padded to be a multiple of the algorithm's block size. Once ``finalize`` is called this object can no longer be used and :meth:`update` and :meth:`finalize` will raise @@ -473,7 +455,7 @@ Interfaces :return bytes: Returns the tag value as bytes. :raises: :class:`~cryptography.exceptions.NotYetFinalized` if called - before the context is finalized. + before the context is finalized. .. _`described by Colin Percival`: http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html diff --git a/docs/hazmat/primitives/twofactor.rst b/docs/hazmat/primitives/twofactor.rst index 9d661612..3df1a147 100644 --- a/docs/hazmat/primitives/twofactor.rst +++ b/docs/hazmat/primitives/twofactor.rst @@ -13,14 +13,14 @@ codes (HMAC). .. currentmodule:: cryptography.hazmat.primitives.twofactor.hotp -.. class:: HOTP(key, length, backend) +.. class:: HOTP(key, length, algorithm, backend) .. versionadded:: 0.3 - HOTP objects take a ``key`` and ``length`` parameter. The ``key`` - should be randomly generated bytes and is recommended to be 160 bits in - length. The ``length`` parameter controls the length of the generated - one time password and must be >= 6 and <= 8. + HOTP objects take a ``key``, ``length`` and ``algorithm`` parameter. The + ``key`` should be randomly generated bytes and is recommended to be 160 + bits in length. The ``length`` parameter controls the length of the + generated one time password and must be >= 6 and <= 8. This is an implementation of :rfc:`4226`. @@ -29,55 +29,64 @@ codes (HMAC). >>> import os >>> from cryptography.hazmat.backends import default_backend >>> from cryptography.hazmat.primitives.twofactor.hotp import HOTP - - >>> key = b"12345678901234567890" - >>> hotp = HOTP(key, 6, backend=default_backend()) - >>> hotp.generate(0) - '755224' - >>> hotp.verify(b"755224", 0) - - :param bytes key: Secret key as ``bytes``. This value must be generated in a - cryptographically secure fashion and be at least 128 bits. - It is recommended that the key be 160 bits. + >>> from cryptography.hazmat.primitives.hashes import SHA1 + >>> key = os.urandom(16) + >>> hotp = HOTP(key, 6, SHA1(), backend=default_backend()) + >>> hotp_value = hotp.generate(0) + >>> hotp.verify(hotp_value, 0) + + :param bytes key: Per-user secret key. This value must be kept secret + and be at least 128 bits. It is recommended that the + key be 160 bits. :param int length: Length of generated one time password as ``int``. + :param HashAlgorithm algorithm: A + :class:`~cryptography.hazmat.primitives.hashes` + provider. :param backend: A :class:`~cryptography.hazmat.backends.interfaces.HMACBackend` provider. - :raises ValueError: This is raised if the provided ``key`` is shorter 128 bits - or if the ``length`` parameter is not between 6 to 8. - + :raises ValueError: This is raised if the provided ``key`` is shorter than + 128 bits or if the ``length`` parameter is not 6, 7 or 8. + :raises UnsupportedAlgorithm: This is raised if the provided ``algorithm`` + is not :class:`~cryptography.hazmat.primitives.hashes.SHA1()`, + :class:`~cryptography.hazmat.primitives.hashes.SHA256()` or + :class:`~cryptography.hazmat.primitives.hashes.SHA512()`. .. method:: generate(counter) - :param int counter: The counter value used to generate the one time password. + :param int counter: The counter value used to generate the one time + password. :return bytes: A one time password value. .. method:: verify(hotp, counter) :param bytes hotp: The one time password value to validate. - :param bytes counter: The counter value to validate against. - :raises cryptography.exceptions.InvalidToken: This is raised when the supplied HOTP - does not match the expected HOTP. + :param int counter: The counter value to validate against. + :raises cryptography.exceptions.InvalidToken: This is raised when the + supplied HOTP does not match the expected HOTP. Throttling ----------- +~~~~~~~~~~ -Due to the fact that the HOTP algorithm generates rather short tokens that are 6 - 8 digits -long, brute force attacks are possible. It is highly recommended that the server that -validates the token implement a throttling scheme that locks out the account for a period of -time after a number of failed attempts. The number of allowed attempts should be as low as -possible while still ensuring that usability is not significantly impacted. +Due to the fact that the HOTP algorithm generates rather short tokens that are +6 - 8 digits long, brute force attacks are possible. It is highly recommended +that the server that validates the token implement a throttling scheme that +locks out the account for a period of time after a number of failed attempts. +The number of allowed attempts should be as low as possible while still +ensuring that usability is not significantly impacted. Re-synchronization of the Counter ---------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The server's counter value should only be incremented on a successful HOTP authentication. -However, the counter on the client is incremented every time a new HOTP value is requested. -This can lead to the counter value being out of synchronization between the client and server. +The server's counter value should only be incremented on a successful HOTP +authentication. However, the counter on the client is incremented every time a +new HOTP value is requested. This can lead to the counter value being out of +synchronization between the client and server. -Due to this, it is highly recommended that the server sets a look-ahead window that allows the -server to calculate the next ``x`` HOTP values and check them against the supplied HOTP value. -This can be accomplished with something similar to the following code. +Due to this, it is highly recommended that the server sets a look-ahead window +that allows the server to calculate the next ``x`` HOTP values and check them +against the supplied HOTP value. This can be accomplished with something +similar to the following code. .. code-block:: python @@ -86,11 +95,66 @@ This can be accomplished with something similar to the following code. correct_counter = None otp = HOTP(key, 6, default_backend()) - for count in range(counter, counter+look_ahead): + for count in range(counter, counter + look_ahead): try: otp.verify(hotp, count) correct_counter = count except InvalidToken: pass - return correct_counter
\ No newline at end of file + return correct_counter + +.. currentmodule:: cryptography.hazmat.primitives.twofactor.totp + +.. class:: TOTP(key, length, algorithm, time_step, backend) + + TOTP objects take a ``key``, ``length``, ``algorithm`` and ``time_step`` + parameter. The ``key`` should be randomly generated bytes and is recommended + to be as long as your hash function's output (e.g 256-bit for SHA256). + The ``length`` parameter controls the length of the generated one time + password and must be >= 6 and <= 8. + + This is an implementation of :rfc:`6238`. + + .. doctest:: + + >>> import os + >>> import time + >>> from cryptography.hazmat.backends import default_backend + >>> from cryptography.hazmat.primitives.twofactor.totp import TOTP + >>> from cryptography.hazmat.primitives.hashes import SHA1 + >>> key = os.urandom(16) + >>> totp = TOTP(key, 8, SHA1(), 30, backend=default_backend()) + >>> time_value = time.time() + >>> totp_value = totp.generate(time_value) + >>> totp.verify(totp_value, time_value) + + :param bytes key: Per-user secret key. This value must be kept secret + and be at least 128 bits. It is recommended that the + key be 160 bits. + :param int length: Length of generated one time password as ``int``. + :param HashAlgorithm algorithm: A + :class:`~cryptography.hazmat.primitives.hashes` + provider. + :param int time_step: The time step size. The recommended size is 30. + :param backend: A + :class:`~cryptography.hazmat.backends.interfaces.HMACBackend` + provider. + :raises ValueError: This is raised if the provided ``key`` is shorter than + 128 bits or if the ``length`` parameter is not 6, 7 or 8. + :raises UnsupportedAlgorithm: This is raised if the provided ``algorithm`` + is not :class:`~cryptography.hazmat.primitives.hashes.SHA1()`, + :class:`~cryptography.hazmat.primitives.hashes.SHA256()` or + :class:`~cryptography.hazmat.primitives.hashes.SHA512()`. + + .. method:: generate(time) + + :param int time: The time value used to generate the one time password. + :return bytes: A one time password value. + + .. method:: verify(totp, time) + + :param bytes totp: The one time password value to validate. + :param int time: The time value to validate against. + :raises cryptography.exceptions.InvalidToken: This is raised when the + supplied TOTP does not match the expected TOTP. diff --git a/docs/random-numbers.rst b/docs/random-numbers.rst index cd73a7b7..12969d1c 100644 --- a/docs/random-numbers.rst +++ b/docs/random-numbers.rst @@ -8,8 +8,8 @@ want to use the standard :mod:`random` module APIs. This is because they do not provide a cryptographically secure random number generator, which can result in major security issues depending on the algorithms in use. -Therefore, it is our recommendation to always use your operating system's -provided random number generator, which is available as ``os.urandom()``. For +Therefore, it is our recommendation to `always use your operating system's +provided random number generator`_, which is available as ``os.urandom()``. For example, if you need 16 bytes of random data for an initialization vector, you can obtain them with: @@ -18,3 +18,5 @@ can obtain them with: >>> import os >>> os.urandom(16) '...' + +.. _`always use your operating system's provided random number generator`: http://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers/ @@ -11,10 +11,11 @@ # See the License for the specific language governing permissions and # limitations under the License. import os +import sys from distutils.command.build import build from setuptools import setup, find_packages - +from setuptools.command.test import test as TestCommand base_dir = os.path.dirname(__file__) @@ -31,8 +32,14 @@ requirements = [ SIX_DEPENDENCY ] +test_requirements = [ + "pytest", + "pretend", + "iso8601" +] + -class cffi_build(build): +class CFFIBuild(build): """ This class exists, instead of just providing ``ext_modules=[...]`` directly in ``setup()`` because importing cryptography requires we have several @@ -64,6 +71,19 @@ class cffi_build(build): build.finalize_options(self) +class PyTest(TestCommand): + def finalize_options(self): + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + + def run_tests(self): + # Import here because in module scope the eggs are not loaded. + import pytest + errno = pytest.main(self.test_args) + sys.exit(errno) + + with open(os.path.join(base_dir, "README.rst")) as f: long_description = f.read() @@ -105,11 +125,13 @@ setup( install_requires=requirements, setup_requires=requirements, + tests_require=test_requirements, # for cffi zip_safe=False, ext_package="cryptography", cmdclass={ - "build": cffi_build, + "build": CFFIBuild, + "test": PyTest, } ) diff --git a/tests/hazmat/primitives/twofactor/test_hotp.py b/tests/hazmat/primitives/twofactor/test_hotp.py index ec619b55..7c584271 100644 --- a/tests/hazmat/primitives/twofactor/test_hotp.py +++ b/tests/hazmat/primitives/twofactor/test_hotp.py @@ -15,10 +15,11 @@ import os import pytest -from cryptography.exceptions import InvalidToken +from cryptography.exceptions import InvalidToken, UnsupportedAlgorithm from cryptography.hazmat.primitives.twofactor.hotp import HOTP from cryptography.hazmat.primitives import hashes from tests.utils import load_vectors_from_file, load_nist_vectors +from cryptography.hazmat.primitives.hashes import MD5, SHA1 vectors = load_vectors_from_file( "twofactor/rfc-4226.txt", load_nist_vectors) @@ -30,18 +31,23 @@ vectors = load_vectors_from_file( ) @pytest.mark.hmac class TestHOTP(object): - def test_invalid_key_length(self, backend): secret = os.urandom(10) with pytest.raises(ValueError): - HOTP(secret, 6, backend) + HOTP(secret, 6, SHA1(), backend) def test_invalid_hotp_length(self, backend): secret = os.urandom(16) with pytest.raises(ValueError): - HOTP(secret, 4, backend) + HOTP(secret, 4, SHA1(), backend) + + def test_invalid_algorithm(self, backend): + secret = os.urandom(16) + + with pytest.raises(UnsupportedAlgorithm): + HOTP(secret, 6, MD5(), backend) @pytest.mark.parametrize("params", vectors) def test_truncate(self, backend, params): @@ -49,7 +55,7 @@ class TestHOTP(object): counter = int(params["counter"]) truncated = params["truncated"] - hotp = HOTP(secret, 6, backend) + hotp = HOTP(secret, 6, SHA1(), backend) assert hotp._dynamic_truncate(counter) == int(truncated.decode(), 16) @@ -59,7 +65,7 @@ class TestHOTP(object): counter = int(params["counter"]) hotp_value = params["hotp"] - hotp = HOTP(secret, 6, backend) + hotp = HOTP(secret, 6, SHA1(), backend) assert hotp.generate(counter) == hotp_value @@ -69,7 +75,7 @@ class TestHOTP(object): counter = int(params["counter"]) hotp_value = params["hotp"] - hotp = HOTP(secret, 6, backend) + hotp = HOTP(secret, 6, SHA1(), backend) assert hotp.verify(hotp_value, counter) is None @@ -77,7 +83,7 @@ class TestHOTP(object): secret = b"12345678901234567890" counter = 0 - hotp = HOTP(secret, 6, backend) + hotp = HOTP(secret, 6, SHA1(), backend) with pytest.raises(InvalidToken): hotp.verify(b"123456", counter) diff --git a/tests/hazmat/primitives/twofactor/test_totp.py b/tests/hazmat/primitives/twofactor/test_totp.py new file mode 100644 index 00000000..a4a108bc --- /dev/null +++ b/tests/hazmat/primitives/twofactor/test_totp.py @@ -0,0 +1,129 @@ +# 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 +# +# http://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. + +import pytest + +from cryptography.exceptions import InvalidToken +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.twofactor.totp import TOTP +from tests.utils import load_vectors_from_file, load_nist_vectors + +vectors = load_vectors_from_file( + "twofactor/rfc-6238.txt", load_nist_vectors) + + +@pytest.mark.hmac +class TestTOTP(object): + @pytest.mark.supported( + only_if=lambda backend: backend.hmac_supported(hashes.SHA1()), + skip_message="Does not support HMAC-SHA1." + ) + @pytest.mark.parametrize( + "params", [i for i in vectors if i["mode"] == b"SHA1"]) + def test_generate_sha1(self, backend, params): + secret = params["secret"] + time = int(params["time"]) + totp_value = params["totp"] + + totp = TOTP(secret, 8, hashes.SHA1(), 30, backend) + assert totp.generate(time) == totp_value + + @pytest.mark.supported( + only_if=lambda backend: backend.hmac_supported(hashes.SHA256()), + skip_message="Does not support HMAC-SHA256." + ) + @pytest.mark.parametrize( + "params", [i for i in vectors if i["mode"] == b"SHA256"]) + def test_generate_sha256(self, backend, params): + secret = params["secret"] + time = int(params["time"]) + totp_value = params["totp"] + + totp = TOTP(secret, 8, hashes.SHA256(), 30, backend) + assert totp.generate(time) == totp_value + + @pytest.mark.supported( + only_if=lambda backend: backend.hmac_supported(hashes.SHA512()), + skip_message="Does not support HMAC-SHA512." + ) + @pytest.mark.parametrize( + "params", [i for i in vectors if i["mode"] == b"SHA512"]) + def test_generate_sha512(self, backend, params): + secret = params["secret"] + time = int(params["time"]) + totp_value = params["totp"] + + totp = TOTP(secret, 8, hashes.SHA512(), 30, backend) + assert totp.generate(time) == totp_value + + @pytest.mark.supported( + only_if=lambda backend: backend.hmac_supported(hashes.SHA1()), + skip_message="Does not support HMAC-SHA1." + ) + @pytest.mark.parametrize( + "params", [i for i in vectors if i["mode"] == b"SHA1"]) + def test_verify_sha1(self, backend, params): + secret = params["secret"] + time = int(params["time"]) + totp_value = params["totp"] + + totp = TOTP(secret, 8, hashes.SHA1(), 30, backend) + + assert totp.verify(totp_value, time) is None + + @pytest.mark.supported( + only_if=lambda backend: backend.hmac_supported(hashes.SHA256()), + skip_message="Does not support HMAC-SHA256." + ) + @pytest.mark.parametrize( + "params", [i for i in vectors if i["mode"] == b"SHA256"]) + def test_verify_sha256(self, backend, params): + secret = params["secret"] + time = int(params["time"]) + totp_value = params["totp"] + + totp = TOTP(secret, 8, hashes.SHA256(), 30, backend) + + assert totp.verify(totp_value, time) is None + + @pytest.mark.supported( + only_if=lambda backend: backend.hmac_supported(hashes.SHA512()), + skip_message="Does not support HMAC-SHA512." + ) + @pytest.mark.parametrize( + "params", [i for i in vectors if i["mode"] == b"SHA512"]) + def test_verify_sha512(self, backend, params): + secret = params["secret"] + time = int(params["time"]) + totp_value = params["totp"] + + totp = TOTP(secret, 8, hashes.SHA512(), 30, backend) + + assert totp.verify(totp_value, time) is None + + def test_invalid_verify(self, backend): + secret = b"12345678901234567890" + time = 59 + + totp = TOTP(secret, 8, hashes.SHA1(), 30, backend) + + with pytest.raises(InvalidToken): + totp.verify(b"12345678", time) + + def test_floating_point_time_generate(self, backend): + secret = b"12345678901234567890" + time = 59.1 + + totp = TOTP(secret, 8, hashes.SHA1(), 30, backend) + + assert totp.generate(time) == b"94287082" @@ -41,3 +41,4 @@ commands = flake8 . [flake8] exclude = .tox,*.egg +select = E,W,F |