Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/hazmat/primitives/twofactor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ codes (HMAC).
:raises cryptography.hazmat.primitives.twofactor.InvalidToken: This
is raised when the supplied HOTP does not match the expected HOTP.

.. method:: get_provisioning_uri(account_name, counter, issuer)

:param str account_name: The display name of account, such as
``'Alice Smith'`` or ``'alice@example.com'``.
:param issuer: The optional display name of issuer.
:type issuer: `string` or `None`
:param int counter: The current value of counter.
:return str: An URI string.

Throttling
~~~~~~~~~~

Expand Down Expand Up @@ -171,3 +180,35 @@ similar to the following code.
:param int time: The time value to validate against.
:raises cryptography.hazmat.primitives.twofactor.InvalidToken: This
is raised when the supplied TOTP does not match the expected TOTP.

.. method:: get_provisioning_uri(account_name, issuer)

:param str account_name: The display name of account, such as
``'Alice Smith'`` or ``'alice@example.com'``.
:param issuer: The optional display name of issuer.
:type issuer: `string` or `None`
:return str: An URI string.

Provisioning URI
~~~~~~~~~~~~~~~~

The provisioning URI of HOTP and TOTP is not actual the part of RFC 4226 and
RFC 6238, but a `spec of Google Authenticator`_. It is widely supported by web
sites and mobile applications which are using Two-Factor authentication.

For generating a provisioning URI, you could use the ``get_provisioning_uri``
method of HOTP/TOTP instances.

.. code-block:: python

counter = 5
account_name = 'alice@example.com'
issuer_name = 'Example Inc'

hotp_uri = hotp.get_provisioning_uri(account_name, counter, issuer_name)
totp_uri = totp.get_provisioning_uri(account_name, issuer_name)

A common usage is encoding the provisioning URI into QR code and guiding users
to scan it with Two-Factor authentication applications in their mobile devices.

.. _`spec of Google Authenticator`: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
6 changes: 6 additions & 0 deletions src/cryptography/hazmat/primitives/twofactor/hotp.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from cryptography.hazmat.primitives import constant_time, hmac
from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SHA512
from cryptography.hazmat.primitives.twofactor import InvalidToken
from cryptography.hazmat.primitives.twofactor.utils import _generate_uri


class HOTP(object):
Expand Down Expand Up @@ -59,3 +60,8 @@ def _dynamic_truncate(self, counter):
offset = six.indexbytes(hmac_value, len(hmac_value) - 1) & 0b1111
p = hmac_value[offset:offset + 4]
return struct.unpack(">I", p)[0] & 0x7fffffff

def get_provisioning_uri(self, account_name, counter, issuer):
return _generate_uri(self, 'hotp', account_name, issuer, [
('counter', int(counter)),
])
6 changes: 6 additions & 0 deletions src/cryptography/hazmat/primitives/twofactor/totp.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from cryptography.hazmat.primitives import constant_time
from cryptography.hazmat.primitives.twofactor import InvalidToken
from cryptography.hazmat.primitives.twofactor.hotp import HOTP
from cryptography.hazmat.primitives.twofactor.utils import _generate_uri


class TOTP(object):
Expand All @@ -31,3 +32,8 @@ def generate(self, time):
def verify(self, totp, time):
if not constant_time.bytes_eq(self.generate(time), totp):
raise InvalidToken("Supplied TOTP value does not match.")

def get_provisioning_uri(self, account_name, issuer):
return _generate_uri(self._hotp, 'totp', account_name, issuer, [
('period', int(self._time_step)),
])
30 changes: 30 additions & 0 deletions src/cryptography/hazmat/primitives/twofactor/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

from __future__ import absolute_import, division, print_function

import base64

from six.moves.urllib.parse import quote, urlencode


def _generate_uri(hotp, type_name, account_name, issuer, extra_parameters):
parameters = [
('digits', hotp._length),
('secret', base64.b32encode(hotp._key)),
('algorithm', hotp._algorithm.name.upper()),
]

if issuer is not None:
parameters.append(('issuer', issuer))

parameters.extend(extra_parameters)

uriparts = {
'type': type_name,
'label': ('%s:%s' % (quote(issuer), quote(account_name)) if issuer
else quote(account_name)),
'parameters': urlencode(parameters),
}
return 'otpauth://{type}/{label}?{parameters}'.format(**uriparts)
13 changes: 13 additions & 0 deletions tests/hazmat/primitives/twofactor/test_hotp.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ def test_length_not_int(self, backend):
with pytest.raises(TypeError):
HOTP(secret, b"foo", SHA1(), backend)

def test_get_provisioning_uri(self, backend):
secret = b"12345678901234567890"
hotp = HOTP(secret, 6, SHA1(), backend)

assert hotp.get_provisioning_uri("Alice Smith", 1, None) == (
"otpauth://hotp/Alice%20Smith?digits=6&secret=GEZDGNBV"
"GY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&counter=1")

assert hotp.get_provisioning_uri("Alice Smith", 1, 'Foo') == (
"otpauth://hotp/Foo:Alice%20Smith?digits=6&secret=GEZD"
"GNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&issuer=Foo"
"&counter=1")


def test_invalid_backend():
secret = b"12345678901234567890"
Expand Down
13 changes: 13 additions & 0 deletions tests/hazmat/primitives/twofactor/test_totp.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,19 @@ def test_floating_point_time_generate(self, backend):

assert totp.generate(time) == b"94287082"

def test_get_provisioning_uri(self, backend):
secret = b"12345678901234567890"
totp = TOTP(secret, 6, hashes.SHA1(), 30, backend=backend)

assert totp.get_provisioning_uri("Alice Smith", None) == (
"otpauth://totp/Alice%20Smith?digits=6&secret=GEZDGNBVG"
"Y3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&period=30")

assert totp.get_provisioning_uri("Alice Smith", 'World') == (
"otpauth://totp/World:Alice%20Smith?digits=6&secret=GEZ"
"DGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&issuer=World"
"&period=30")


def test_invalid_backend():
secret = b"12345678901234567890"
Expand Down