From 8c1ad596b02f89cde6040e8626e07ca352182130 Mon Sep 17 00:00:00 2001 From: Ayrx Date: Tue, 18 Feb 2014 12:33:55 +0800 Subject: Changed module name from otp to twofactor. --- cryptography/hazmat/primitives/otp/__init__.py | 0 cryptography/hazmat/primitives/otp/hotp.py | 54 ------------ .../hazmat/primitives/twofactor/__init__.py | 0 cryptography/hazmat/primitives/twofactor/hotp.py | 54 ++++++++++++ docs/hazmat/primitives/index.rst | 2 +- docs/hazmat/primitives/otp.rst | 95 ---------------------- docs/hazmat/primitives/twofactor.rst | 94 +++++++++++++++++++++ tests/hazmat/primitives/otp/__init__.py | 0 tests/hazmat/primitives/otp/test_hotp.py | 83 ------------------- tests/hazmat/primitives/twofactor/__init__.py | 0 tests/hazmat/primitives/twofactor/test_hotp.py | 83 +++++++++++++++++++ 11 files changed, 232 insertions(+), 233 deletions(-) delete mode 100644 cryptography/hazmat/primitives/otp/__init__.py delete mode 100644 cryptography/hazmat/primitives/otp/hotp.py create mode 100644 cryptography/hazmat/primitives/twofactor/__init__.py create mode 100644 cryptography/hazmat/primitives/twofactor/hotp.py delete mode 100644 docs/hazmat/primitives/otp.rst create mode 100644 docs/hazmat/primitives/twofactor.rst delete mode 100644 tests/hazmat/primitives/otp/__init__.py delete mode 100644 tests/hazmat/primitives/otp/test_hotp.py create mode 100644 tests/hazmat/primitives/twofactor/__init__.py create mode 100644 tests/hazmat/primitives/twofactor/test_hotp.py diff --git a/cryptography/hazmat/primitives/otp/__init__.py b/cryptography/hazmat/primitives/otp/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/cryptography/hazmat/primitives/otp/hotp.py b/cryptography/hazmat/primitives/otp/hotp.py deleted file mode 100644 index 0bc4cc56..00000000 --- a/cryptography/hazmat/primitives/otp/hotp.py +++ /dev/null @@ -1,54 +0,0 @@ -# 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 struct - -import six - -from cryptography.exceptions import InvalidToken -from cryptography.hazmat.primitives import constant_time, hmac -from cryptography.hazmat.primitives.hashes import SHA1 - - -class HOTP(object): - def __init__(self, key, length, 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.") - - self._key = key - self._length = length - self._backend = backend - - def generate(self, counter): - truncated_value = self._dynamic_truncate(counter) - hotp = truncated_value % (10**self._length) - return "{0:0{1}}".format(hotp, self._length).encode() - - def verify(self, hotp, counter): - if not constant_time.bytes_eq(self.generate(counter), hotp): - raise InvalidToken("Supplied HOTP value does not match") - - def _dynamic_truncate(self, counter): - ctx = hmac.HMAC(self._key, SHA1(), self._backend) - ctx.update(struct.pack(">Q", counter)) - hmac_value = ctx.finalize() - - offset_bits = six.indexbytes(hmac_value, 19) & 0b1111 - - offset = int(offset_bits) - P = hmac_value[offset:offset+4] - return struct.unpack(">I", P)[0] & 0x7fffffff diff --git a/cryptography/hazmat/primitives/twofactor/__init__.py b/cryptography/hazmat/primitives/twofactor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cryptography/hazmat/primitives/twofactor/hotp.py b/cryptography/hazmat/primitives/twofactor/hotp.py new file mode 100644 index 00000000..0bc4cc56 --- /dev/null +++ b/cryptography/hazmat/primitives/twofactor/hotp.py @@ -0,0 +1,54 @@ +# 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 struct + +import six + +from cryptography.exceptions import InvalidToken +from cryptography.hazmat.primitives import constant_time, hmac +from cryptography.hazmat.primitives.hashes import SHA1 + + +class HOTP(object): + def __init__(self, key, length, 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.") + + self._key = key + self._length = length + self._backend = backend + + def generate(self, counter): + truncated_value = self._dynamic_truncate(counter) + hotp = truncated_value % (10**self._length) + return "{0:0{1}}".format(hotp, self._length).encode() + + def verify(self, hotp, counter): + if not constant_time.bytes_eq(self.generate(counter), hotp): + raise InvalidToken("Supplied HOTP value does not match") + + def _dynamic_truncate(self, counter): + ctx = hmac.HMAC(self._key, SHA1(), self._backend) + ctx.update(struct.pack(">Q", counter)) + hmac_value = ctx.finalize() + + offset_bits = six.indexbytes(hmac_value, 19) & 0b1111 + + offset = int(offset_bits) + P = hmac_value[offset:offset+4] + return struct.unpack(">I", P)[0] & 0x7fffffff diff --git a/docs/hazmat/primitives/index.rst b/docs/hazmat/primitives/index.rst index 9121d156..5199d493 100644 --- a/docs/hazmat/primitives/index.rst +++ b/docs/hazmat/primitives/index.rst @@ -14,4 +14,4 @@ Primitives rsa constant-time interfaces - otp + twofactor diff --git a/docs/hazmat/primitives/otp.rst b/docs/hazmat/primitives/otp.rst deleted file mode 100644 index ad2c856a..00000000 --- a/docs/hazmat/primitives/otp.rst +++ /dev/null @@ -1,95 +0,0 @@ -.. hazmat:: - -One Time Password -================= - -.. currentmodule:: cryptography.hazmat.primitives.otp - -This module contains algorithms for generating and verifying one -time passwords. - -Currently, it contains an algorithm for generating and verifying -one time password values based on Hash-based message authentication -codes (HMAC). - -.. currentmodule:: cryptography.hazmat.primitives.otp.hotp - -.. class:: HOTP(key, length, backend) - - 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. - - This is an implementation of :rfc:`4226`. - - .. doctest:: - - >>> import os - >>> from cryptography.hazmat.backends import default_backend - >>> from cryptography.hazmat.primitives.otp.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. - :param int length: Length of generated one time password as ``int``. - :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. - - - .. method:: generate(counter) - - :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. - -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. - -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. - -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 - - def verify(hotp, counter, look_ahead): - assert look_ahead >= 0 - correct_counter = None - - otp = HOTP(key, 6, default_backend()) - 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 diff --git a/docs/hazmat/primitives/twofactor.rst b/docs/hazmat/primitives/twofactor.rst new file mode 100644 index 00000000..2b811e1e --- /dev/null +++ b/docs/hazmat/primitives/twofactor.rst @@ -0,0 +1,94 @@ +.. hazmat:: + +Two-factor Authentication +========================= + +.. currentmodule:: cryptography.hazmat.primitives.twofactor + +This module contains algorithms related to two-factor authentication. + +Currently, it contains an algorithm for generating and verifying +one time password values based on Hash-based message authentication +codes (HMAC). + +.. currentmodule:: cryptography.hazmat.primitives.twofactor.hotp + +.. class:: HOTP(key, length, backend) + + 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. + + This is an implementation of :rfc:`4226`. + + .. doctest:: + + >>> 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. + :param int length: Length of generated one time password as ``int``. + :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. + + + .. method:: generate(counter) + + :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. + +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. + +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. + +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 + + def verify(hotp, counter, look_ahead): + assert look_ahead >= 0 + correct_counter = None + + otp = HOTP(key, 6, default_backend()) + 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 diff --git a/tests/hazmat/primitives/otp/__init__.py b/tests/hazmat/primitives/otp/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/hazmat/primitives/otp/test_hotp.py b/tests/hazmat/primitives/otp/test_hotp.py deleted file mode 100644 index 2641cb99..00000000 --- a/tests/hazmat/primitives/otp/test_hotp.py +++ /dev/null @@ -1,83 +0,0 @@ -# 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 os - -import pytest - -from cryptography.exceptions import InvalidToken -from cryptography.hazmat.primitives.otp.hotp import HOTP -from cryptography.hazmat.primitives import hashes -from tests.utils import load_vectors_from_file, load_nist_vectors - -vectors = load_vectors_from_file( - "oath/rfc-4226.txt", load_nist_vectors) - - -@pytest.mark.supported( - only_if=lambda backend: backend.hmac_supported(hashes.SHA1()), - skip_message="Does not support HMAC-SHA1." -) -@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) - - def test_invalid_hotp_length(self, backend): - secret = os.urandom(16) - - with pytest.raises(ValueError): - HOTP(secret, 4, backend) - - @pytest.mark.parametrize("params", vectors) - def test_truncate(self, backend, params): - secret = params["secret"] - counter = int(params["counter"]) - truncated = params["truncated"] - - hotp = HOTP(secret, 6, backend) - - assert hotp._dynamic_truncate(counter) == int(truncated.decode(), 16) - - @pytest.mark.parametrize("params", vectors) - def test_generate(self, backend, params): - secret = params["secret"] - counter = int(params["counter"]) - hotp_value = params["hotp"] - - hotp = HOTP(secret, 6, backend) - - assert hotp.generate(counter) == hotp_value - - @pytest.mark.parametrize("params", vectors) - def test_verify(self, backend, params): - secret = params["secret"] - counter = int(params["counter"]) - hotp_value = params["hotp"] - - hotp = HOTP(secret, 6, backend) - - assert hotp.verify(hotp_value, counter) is None - - def test_invalid_verify(self, backend): - secret = b"12345678901234567890" - counter = 0 - - hotp = HOTP(secret, 6, backend) - - with pytest.raises(InvalidToken): - hotp.verify(b"123456", counter) diff --git a/tests/hazmat/primitives/twofactor/__init__.py b/tests/hazmat/primitives/twofactor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/hazmat/primitives/twofactor/test_hotp.py b/tests/hazmat/primitives/twofactor/test_hotp.py new file mode 100644 index 00000000..25dd694b --- /dev/null +++ b/tests/hazmat/primitives/twofactor/test_hotp.py @@ -0,0 +1,83 @@ +# 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 os + +import pytest + +from cryptography.exceptions import InvalidToken +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 + +vectors = load_vectors_from_file( + "oath/rfc-4226.txt", load_nist_vectors) + + +@pytest.mark.supported( + only_if=lambda backend: backend.hmac_supported(hashes.SHA1()), + skip_message="Does not support HMAC-SHA1." +) +@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) + + def test_invalid_hotp_length(self, backend): + secret = os.urandom(16) + + with pytest.raises(ValueError): + HOTP(secret, 4, backend) + + @pytest.mark.parametrize("params", vectors) + def test_truncate(self, backend, params): + secret = params["secret"] + counter = int(params["counter"]) + truncated = params["truncated"] + + hotp = HOTP(secret, 6, backend) + + assert hotp._dynamic_truncate(counter) == int(truncated.decode(), 16) + + @pytest.mark.parametrize("params", vectors) + def test_generate(self, backend, params): + secret = params["secret"] + counter = int(params["counter"]) + hotp_value = params["hotp"] + + hotp = HOTP(secret, 6, backend) + + assert hotp.generate(counter) == hotp_value + + @pytest.mark.parametrize("params", vectors) + def test_verify(self, backend, params): + secret = params["secret"] + counter = int(params["counter"]) + hotp_value = params["hotp"] + + hotp = HOTP(secret, 6, backend) + + assert hotp.verify(hotp_value, counter) is None + + def test_invalid_verify(self, backend): + secret = b"12345678901234567890" + counter = 0 + + hotp = HOTP(secret, 6, backend) + + with pytest.raises(InvalidToken): + hotp.verify(b"123456", counter) -- cgit v1.2.3