1# This file is dual licensed under the terms of the Apache License, Version 2# 2.0, and the BSD License. See the LICENSE file in the root of this repository 3# for complete details. 4 5from __future__ import absolute_import, division, print_function 6 7import os 8 9import pytest 10 11from cryptography.exceptions import _Reasons 12from cryptography.hazmat.backends.interfaces import HMACBackend 13from cryptography.hazmat.primitives import hashes 14from cryptography.hazmat.primitives.hashes import MD5, SHA1 15from cryptography.hazmat.primitives.twofactor import InvalidToken 16from cryptography.hazmat.primitives.twofactor.hotp import HOTP 17 18from ....utils import ( 19 load_nist_vectors, 20 load_vectors_from_file, 21 raises_unsupported_algorithm, 22) 23 24vectors = load_vectors_from_file("twofactor/rfc-4226.txt", load_nist_vectors) 25 26 27@pytest.mark.supported( 28 only_if=lambda backend: backend.hmac_supported(hashes.SHA1()), 29 skip_message="Does not support HMAC-SHA1.", 30) 31@pytest.mark.requires_backend_interface(interface=HMACBackend) 32class TestHOTP(object): 33 def test_invalid_key_length(self, backend): 34 secret = os.urandom(10) 35 36 with pytest.raises(ValueError): 37 HOTP(secret, 6, SHA1(), backend) 38 39 def test_unenforced_invalid_kwy_length(self, backend): 40 secret = os.urandom(10) 41 HOTP(secret, 6, SHA1(), backend, enforce_key_length=False) 42 43 def test_invalid_hotp_length(self, backend): 44 secret = os.urandom(16) 45 46 with pytest.raises(ValueError): 47 HOTP(secret, 4, SHA1(), backend) 48 49 def test_invalid_algorithm(self, backend): 50 secret = os.urandom(16) 51 52 with pytest.raises(TypeError): 53 HOTP(secret, 6, MD5(), backend) 54 55 @pytest.mark.parametrize("params", vectors) 56 def test_truncate(self, backend, params): 57 secret = params["secret"] 58 counter = int(params["counter"]) 59 truncated = params["truncated"] 60 61 hotp = HOTP(secret, 6, SHA1(), backend) 62 63 assert hotp._dynamic_truncate(counter) == int(truncated.decode(), 16) 64 65 @pytest.mark.parametrize("params", vectors) 66 def test_generate(self, backend, params): 67 secret = params["secret"] 68 counter = int(params["counter"]) 69 hotp_value = params["hotp"] 70 71 hotp = HOTP(secret, 6, SHA1(), backend) 72 73 assert hotp.generate(counter) == hotp_value 74 75 @pytest.mark.parametrize("params", vectors) 76 def test_verify(self, backend, params): 77 secret = params["secret"] 78 counter = int(params["counter"]) 79 hotp_value = params["hotp"] 80 81 hotp = HOTP(secret, 6, SHA1(), backend) 82 83 assert hotp.verify(hotp_value, counter) is None 84 85 def test_invalid_verify(self, backend): 86 secret = b"12345678901234567890" 87 counter = 0 88 89 hotp = HOTP(secret, 6, SHA1(), backend) 90 91 with pytest.raises(InvalidToken): 92 hotp.verify(b"123456", counter) 93 94 def test_length_not_int(self, backend): 95 secret = b"12345678901234567890" 96 97 with pytest.raises(TypeError): 98 HOTP(secret, b"foo", SHA1(), backend) 99 100 def test_get_provisioning_uri(self, backend): 101 secret = b"12345678901234567890" 102 hotp = HOTP(secret, 6, SHA1(), backend) 103 104 assert hotp.get_provisioning_uri("Alice Smith", 1, None) == ( 105 "otpauth://hotp/Alice%20Smith?digits=6&secret=GEZDGNBV" 106 "GY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&counter=1" 107 ) 108 109 assert hotp.get_provisioning_uri("Alice Smith", 1, "Foo") == ( 110 "otpauth://hotp/Foo:Alice%20Smith?digits=6&secret=GEZD" 111 "GNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&issuer=Foo" 112 "&counter=1" 113 ) 114 115 def test_buffer_protocol(self, backend): 116 key = bytearray(b"a long key with lots of entropy goes here") 117 hotp = HOTP(key, 6, SHA1(), backend) 118 assert hotp.generate(10) == b"559978" 119 120 121def test_invalid_backend(): 122 secret = b"12345678901234567890" 123 124 pretend_backend = object() 125 126 with raises_unsupported_algorithm(_Reasons.BACKEND_MISSING_INTERFACE): 127 HOTP(secret, 8, hashes.SHA1(), pretend_backend) 128